硬件
不同的独立硬件供应商 (IHV) 对异构 CPU 拓扑的定义各不相同。Intel 和 Qualcomm 使用 Performance (性能) 和 Efficiency (能效) 这两个术语,而 AMD 使用 Performance (性能) 和 Compact (紧凑) 这两个术语。每个供应商在设计其核心拓扑时都会在性能和能效之间做出权衡。 以下示例说明了其中涉及的一些设计选择:- 不同的缓存大小
- 不同的核心可能具有不同的缓存大小。
- 例如,性能核心可能具有更大的 L1 和 L2 缓存。
- 缓存共享
- 核心之间的缓存共享程度可能各不相同。
- 例如,较高性能的核心可能各自拥有专用的 L2 缓存,而能效更高的核心可能共享单个 L2 缓存。
- 对同步多线程 (SMT) 的支持
- 某些核心可能支持 SMT,而其他核心不支持。
- Intel 将 SMT 称为_超线程 (Hyper-Threading)_。
- 内部核心布局
- 各种核心类型的内部资源可能不同。
- 例如,性能核心可能包含更多的浮点管线、ALU 管线或更宽的数据通道。
- 最大频率
- 不同核心类型之间的最大频率可能不同。
- 例如,性能核心可能能够达到比能效核心更高的频率。
性能数据
可以预期,不同能效级别的核心性能会有所不同。一种核心专为性能而设计,另一种则专为节能而设计。问题在于这些核心之间的差异有多大。 所显示的数据是多个 IHV 的汇总。每个 IHV 根据其拓扑设计选择,在每项测试中的缩放比例都不同。虽然确切数字有所不同,但所有 IHV 的模式都相似。此外,每一代 CPU 都会做出不同的权衡,从而调整其相对数值。为所有现有 IHV 和各代产品生成数据超出了本文的范围。综合测试
向量数学是游戏在一帧内所完成工作的主要部分。此工作负载可以是从物理到 AI 再到渲染的任何内容。游戏使用包含三个或四个浮点数的向量类型来执行此工作,这些向量可以放入单个 128 位 SSE 寄存器中。它们涵盖了广泛的运算,例如点积、矩阵乘法、平方根计算和向量变换。这些运算中的性能差异可能会对整体帧时间产生重大影响。 下图显示了执行七种常见数学运算的相对开销。测量使用存储在单个 128 位 SSE 寄存器中的四个 32 位浮点值组成的向量。一个矩阵由四个向量组成,共计十六个 32 位浮点值,分布在四个 128 位 SSE 寄存器中。之所以选择这些运算,是因为它们在一帧中很常用。- 点积:计算向量的点积。
- 变换向量:通过将向量与矩阵相乘来变换向量。
- 向量加法:将两个向量相加。
- 叉积:计算两个向量的叉积。
- 向量正弦:计算向量中每个元素的正弦近似值。
- 向量 FMA (融合乘加):将两个向量相乘并加上第三个向量。
- 平方根倒数:计算向量长度的平方根倒数。

实际场景
前面的数据显示的是孤立的特定基准测试,只反映了部分情况。以下数据来自一个 AAA 游戏引擎的基准测试,该引擎使用两个主线程 (模拟和渲染) 以及一组作业线程。基准测试使用三种测试配置。- 两个主线程仅在较高性能的核心上运行。
- 两个主线程仅在能效核心上运行。
- 两个主线程在系统中的所有核心上运行。

每个制造商都可以调整这些调度程序启发式方法,使其适合自己的硬件。例如,AMD 有其工作负载配置文件调度 (Workload Profile Scheduling),Intel 有其线程调度器 (Thread Director) 技术。每个 IHV 还往往会调整启发式方法,以匹配 SoC 的特定拓扑。
建议
线程关联
在仅有单一能效级别的台式机上运行时,关于线程关联的建议是避免设置硬性线程关联,而是让线程在所有核心之间浮动。造成这种不确定性的主要原因是,无法预先知道用户计算机上可能还运行着哪些其他进程。具有硬性关联的线程有可能最终与系统中另一个高优先级线程共享一个核心。最终结果是游戏线程得不到执行时间。如果这种争用发生在帧关键线程上,游戏可能会出现帧时间不一致的情况。你需要确保操作系统有地方可以移动游戏线程,使其保持运行。 不过,在异构 CPU 上运行时,需要改变这一建议。主要原因是操作系统不一定知道游戏的哪些线程或作业是帧关键的。它有一些启发式方法来确定它认为的帧关键线程,但这些启发式方法仍可能做出错误的选择。它可能决定将其中某个线程或作业移至性能较低的核心,从而使游戏出现性能问题的可能性更高。 建议的目标是允许游戏线程至少在三到四个核心上运行,越多越好。例如,在具有一个较高性能核心和三个较低性能核心的低端系统上,应允许线程在所有核心之间浮动。在具有三个或更多性能核心的计算机上,可以将帧关键线程限制为仅在这些较高性能的核心上运行。 在具有许多核心且同时支持 SMT 的高端计算机上,某些游戏可能会尝试将其线程锁定到物理核心。此技术可避免在物理核心级别发生争用,因为多个高优先级线程会共享物理核心的相同资源。不过,一般不建议考虑这种情况。操作系统已经在系统范围内应用了这种行为。它首先跨物理核心调度线程,仅在必要时才使用更多逻辑核心。 游戏唯一应考虑的情况是,系统中有足够多的较高性能核心来支持游戏的关键线程。在这种情况下,可能需要将帧关键线程锁定到性能较高的核心。但是,应允许这些线程在所有较高性能的核心之间浮动。虽然在某些情况下允许所有线程在所有核心之间浮动可以提高性能,但这种行为在所有 CPU 制造商之间并不一致。 如果游戏实现了能够在所有线程之间完全平衡工作负载的工作窃取系统,则应允许任何作业线程在所有核心之间浮动,无论它们是高优先级还是低优先级作业线程。工作窃取算法会自动平衡负载,使最多的工作在最短的时间内完成。性能较高核心上的线程在帧期间会执行更多作业。 如果有多个帧关键线程,则设置它们的关联以使任何两个帧关键线程都不能在同一核心上运行可能会很有用。例如,如果有两个帧关键线程,将它们的关联设置为彼此互斥,可以消除操作系统导致这两个线程相互争用的可能性。不过同样,只有在有足够的核心支持这种情况,同时仍允许每个帧关键线程至少在三到四个核心之间浮动时,才应考虑此选项。线程拓扑
在考虑如何在异构环境中将工作分配给线程时,主要数据围绕各种任务的时长以及它们之间的交互方式,例如每个任务的平均时长是多少、任务之间的依赖关系是什么、每个任务的优先级是什么等。 作业线程能在多大程度上吸收任何关键线程或作业上的时间激增?如果其中某个线程或作业的运行速度慢 35%,对帧时间的影响有多大?例如,如果某个任务运行时间过长,这一延迟会对帧的其余部分产生多大影响,系统是否会停滞直到该任务完成? 通过在关键线程或作业中增加额外开销来确定异构系统对帧时间的影响程度,可能会很有用。作为测试,可以随机让某个作业或工作块多花费 25%-35% 的时间。整体帧时间的变化程度表明作业系统吸收执行激增的能力。理想情况是帧时间几乎没有变化,这意味着系统可以吸收任意的帧时间激增。这些激增可能来自在性能较低的核心上运行,也可能来自系统中另一个随机进程占用 CPU 时间。线程通信
使用锁定基元进行线程通信时应进行一些考虑。上下文切换的开销与新线程开始运行之前接收核心的 C-State 密切相关。核心需要处于 C0 (运行状态) 才能开始运行新线程。开销最低的情况是核心已在运行,即已处于 C0。但是,如果核心处于空闲状态,它就处于较低的 C-State。C-State 的级别对应于更深层次的睡眠。睡眠越深,核心消耗的电量越少。但是,从更深的睡眠中唤醒并重新开始运行的开销更高。最常见的低功耗 C-State 是 C1,它可能增加 2-7 μs 的开销。下一个 C-State,即 C2,可能增加大约 40-100 μs 的开销。确切数值因 IHV 和 SoC 而异。有许多变量控制核心在空闲时处于何种 C-State,这是响应能力与功耗之间的平衡。较低的 C-State 比较高的 C-State 消耗更少的电量。核心通常会先进入 C1,如果有一段时间没有运行代码,再切换到 C2。 还有另一个问题:当提交作业的线程和执行作业的线程位于具有不同性能配置的核心上时会发生什么?从技术上讲,作业线程完成作业的速度有可能快于新作业的创建速度。如果执行作业的线程所在的核心比提交作业的线程所在的核心更快,这种情况更有可能发生。 如果一个作业的执行时间少于其创建时间,游戏可能会陷入作业线程不断挂起以等待新作业的情况。最终结果是,由于作业线程不断挂起,作业的执行开销变得更高。上下文切换的开销会被加到每个作业的执行时间中。这种开销可能会自相矛盾地导致在性能更高的核心上运行时帧速率反而更低。例如,原本需要 250 μs 的工作可能突然开始需要 275 μs,这种退化意味着性能下降了 10%。 由于这种情况,建议以批处理方式提交作业,而不是在创建时逐个提交。例如,如果系统遵循构建一个作业、提交该作业、再构建一个作业、再提交一个作业等模式,则应将此模式调整为先执行一批构建,然后同时提交所有作业。 如果游戏使用需要 WaitForSingleObject 的对象,则应将其放在一个小型自旋循环中。以超时值 0 调用该函数,自旋完成后再使用游戏的常规超时值。不过,此问题的最佳解决方案是使用现代同步 API,例如轻量级读取器/写入器 (SRW) 锁、临界区和 WaitOnAddress。这些同步 API 内部都包含某种形式的自旋,可以防止线程不断挂起。对于持有时间较短的锁,这种自旋尤其有用。 务必避免长时间的自旋锁。例如,线程不断自旋等待要执行的工作的模式。如果核心上还有其他工作需要执行,调度程序最终会挂起自旋线程以执行这些工作。发生这种抢占时,被挂起的线程很可能会在相当长的时间内保持挂起状态。这种行为都是为了防止其他线程出现饥饿。由于游戏几乎无法控制挂起发生的时间,这可能会导致帧速率卡顿。作业系统
为了获得最佳性能,请围绕作业构建引擎,而不是依赖专用的、长时间运行的帧关键线程。例如,模拟/渲染线程模型会创建两个持久的帧关键线程。但是,耗时较长的作业会导致任何作业线程表现得像帧关键线程一样。如果一个作业需要 10 ms,则执行该作业的线程在此期间就会成为帧关键线程,无论其原本的角色是什么。工作窃取
首要建议是在整个作业系统中使用工作窃取,并让所有帧关键线程参与工作窃取算法。例如,如果渲染线程正在等待渲染相关作业完成,它应执行其中的部分作业。 这种行为在许多游戏引擎中已是常见做法。不过,此处仍值得一提,因为这类行为对于游戏引擎吸收由异构核心和可能的操作系统抢占导致的帧时间激增至关重要。 当一个作业的执行时间较长时,会增加帧时间激增的风险。这种情况可能是由于作业在性能较低的核心上运行,也可能是由于 PC 中的其他工作所致。建议引擎的目标是让每个作业大约占用帧时间的 2%。例如,对于以 60 fps 运行的游戏,这一预算意味着每个作业不超过约 333 μs。如有必要,应将多个较小的作业批处理为一个较大的作业,以达到 2% 的目标。这种方法在作业大小、线程通信和吸收激增的能力之间提供了良好的折中。 以下示例展示了耗时较长的帧关键作业对系统性能可能产生的影响。在这些示例中,每个作业包含相同数量的运算。在此示例中,单个作业在慢速核心上需要 375 μs,在快速核心上需要 250 μs。长杆作业的开销是其他作业的 6 倍。 最坏的情况是长杆作业最终落在某个慢速核心上。这可能会对帧时间产生巨大影响。在这种情况下,执行工作的总时间为 2,250 μs,即 375 μs 的六倍。 图 3:在较慢核心上运行的长杆示例。



