跳转到主要内容
CPU 制造商一直在性能、功耗和芯片面积之间进行权衡。原因在于客户有着不同的使用模式和优先级。台式机用户更关注性能,而笔记本电脑用户则更关注电池续航。更小的芯片面积可以意味着更低的生产成本。为了满足这些相互竞争的需求,CPU 制造商设计了_异构架构_,使系统能够动态适应当前的工作负载需求,同时保持成本效益。 CPU 制造商通过使用具有不同能效级别的 CPU 核心来实现异构拓扑。这种方法将现有 CPU 架构中的部分核心替换为更节能的核心。如果用户不需要 CPU 的全部性能,操作系统会关闭性能更高的核心以节省大量电力,从而延长电池续航时间。当需要更高性能时,操作系统可以动态开启性能更高的核心来处理增加的工作负载。

硬件

不同的独立硬件供应商 (IHV) 对异构 CPU 拓扑的定义各不相同。Intel 和 Qualcomm 使用 Performance (性能) 和 Efficiency (能效) 这两个术语,而 AMD 使用 Performance (性能) 和 Compact (紧凑) 这两个术语。每个供应商在设计其核心拓扑时都会在性能和能效之间做出权衡。 以下示例说明了其中涉及的一些设计选择:
  • 不同的缓存大小
    • 不同的核心可能具有不同的缓存大小。
    • 例如,性能核心可能具有更大的 L1 和 L2 缓存。
  • 缓存共享
    • 核心之间的缓存共享程度可能各不相同。
    • 例如,较高性能的核心可能各自拥有专用的 L2 缓存,而能效更高的核心可能共享单个 L2 缓存。
  • 对同步多线程 (SMT) 的支持
    • 某些核心可能支持 SMT,而其他核心不支持。
    • Intel 将 SMT 称为_超线程 (Hyper-Threading)_。
  • 内部核心布局
    • 各种核心类型的内部资源可能不同。
    • 例如,性能核心可能包含更多的浮点管线、ALU 管线或更宽的数据通道。
  • 最大频率
    • 不同核心类型之间的最大频率可能不同。
    • 例如,性能核心可能能够达到比能效核心更高的频率。
这些差异旨在平衡 CPU 内各核心之间的性能和功耗。频率更高的核心具有更高的性能,但代价是功耗增加。多个核心共享同一缓存可以减小芯片尺寸,从而降低功耗,但代价是性能降低。其目标是在性能和功耗方面提供更具动态性的选择。如果用户不需要全部性能,操作系统会切换到能效更高的核心以降低功耗。 需要注意的是,Windows 要求 CPU 上的所有核心都支持相同的指令集架构 (ISA)。此要求意味着,如果 CPU 支持特定的指令集,则 CPU 中的所有核心都支持该指令集。例如,如果 CPU 声明支持 AVX2,则所有核心都支持 AVX2。 有关性能核心与能效核心之间差异的具体详细信息,请参阅各 IHV 的官方文档。

性能数据

可以预期,不同能效级别的核心性能会有所不同。一种核心专为性能而设计,另一种则专为节能而设计。问题在于这些核心之间的差异有多大。 所显示的数据是多个 IHV 的汇总。每个 IHV 根据其拓扑设计选择,在每项测试中的缩放比例都不同。虽然确切数字有所不同,但所有 IHV 的模式都相似。此外,每一代 CPU 都会做出不同的权衡,从而调整其相对数值。为所有现有 IHV 和各代产品生成数据超出了本文的范围。

综合测试

向量数学是游戏在一帧内所完成工作的主要部分。此工作负载可以是从物理到 AI 再到渲染的任何内容。游戏使用包含三个或四个浮点数的向量类型来执行此工作,这些向量可以放入单个 128 位 SSE 寄存器中。它们涵盖了广泛的运算,例如点积、矩阵乘法、平方根计算和向量变换。这些运算中的性能差异可能会对整体帧时间产生重大影响。 下图显示了执行七种常见数学运算的相对开销。测量使用存储在单个 128 位 SSE 寄存器中的四个 32 位浮点值组成的向量。一个矩阵由四个向量组成,共计十六个 32 位浮点值,分布在四个 128 位 SSE 寄存器中。之所以选择这些运算,是因为它们在一帧中很常用。
  • 点积:计算向量的点积。
  • 变换向量:通过将向量与矩阵相乘来变换向量。
  • 向量加法:将两个向量相加。
  • 叉积:计算两个向量的叉积。
  • 向量正弦:计算向量中每个元素的正弦近似值。
  • 向量 FMA (融合乘加):将两个向量相乘并加上第三个向量。
  • 平方根倒数:计算向量长度的平方根倒数。
图 1:执行运算所花费的时间。越低越好。 异构综合基准测试数据 在此图中,y 轴表示执行运算所花费的时间。每个条形表示在所有 IHV 上测得的该运算的时间范围,即在性能核心或能效核心上运行测试时的最短和最长时间。 正如预期的那样,不同核心类型之间的性能有所不同。不过,从这些数据中得出的关键结论是,性能并不会在所有运算和 IHV 之间等比例缩放。虽然各 IHV 结果的变化程度往往相似,但也有一些变化程度更高。变化程度较高的运算主要出现在向量正弦和平方根倒数运算中。这种差异可以追溯到性能核心和能效核心之间的核心架构选择。造成这种差异的确切原因可能来自多个方面。例如,性能更高的核心可能拥有更多的浮点管线,从而可以并行执行更多运算。确切原因因 IHV 和 CPU 代次而异。甚至可能存在某些运算不存在性能差异的配置。归根结底,游戏需要考虑到各 IHV 之间的这种差异。

实际场景

前面的数据显示的是孤立的特定基准测试,只反映了部分情况。以下数据来自一个 AAA 游戏引擎的基准测试,该引擎使用两个主线程 (模拟和渲染) 以及一组作业线程。基准测试使用三种测试配置。
  • 两个主线程仅在较高性能的核心上运行。
  • 两个主线程仅在能效核心上运行。
  • 两个主线程在系统中的所有核心上运行。
在每种测试配置中,作业线程都可以在系统中的所有核心之间自由浮动。渲染工作使用尽可能低的分辨率和质量。此设置可保证游戏在整个测试运行期间都受 CPU 限制。此设置还使模拟线程成为决定帧速率的驱动因素。 此图显示了一帧中每个线程所花费的时间。它同时显示了平均帧时间和 1% 高值。1% 高值是最长的 1% 帧的阈值,所有帧中只有 1% 高于此值。这个数字可以很好地反映卡顿帧的数量。与平均值相差越远,游戏的卡顿就越严重。在所有情况下,值越低表示长帧越少,可见卡顿也越少。 如前所述,所显示的数据是多个 IHV 的汇总。每个 IHV 根据其拓扑设计选择,在每项测试中的缩放比例都不同。虽然确切数字有所不同,但各 IHV 的模式都相似。每一代 CPU 可能会做出不同的权衡,从而调整其相对数值。为所有现有 CPU IHV 和各代产品生成数据超出了本文的范围。 图 2:平均耗时和 1% 高值。越低越好。 异构实际场景基准测试数据 在此图中,y 轴表示一帧所花费的时间。每个条形表示在所有 IHV 上测得的该运算的时间范围,即所测试的每种线程拓扑的最短和最长时间。 操作系统线程调度程序使用启发式方法来确定线程执行的工作量。这些启发式方法是操作系统选择和 IHV 驱动程序调整的组合。调度程序使用此信息来决定特定线程应在哪些核心上执行。当它判定某个线程正在执行大量工作时,调度程序会将该线程移至性能更高的核心。 由于调度程序的启发式方法,可能会出现一些违反直觉的行为:允许关键线程自由浮动时,游戏反而运行得更快。这种行为并不总是发生,它取决于工作负载、核心拓扑和具体的调度程序启发式方法。 在此处测试的情况下,模拟线程每帧的运行时间比渲染线程长。调度程序倾向于将此线程放在性能更高的核心上。它还决定将渲染线程放在性能较低的核心上,因为渲染线程有更多时间处于挂起状态等待工作。最终效果是,这一决定为某个作业线程腾出了一个性能更高的核心。如果渲染线程执行更多工作,则可能不会出现这种情况。 不过,此图中最值得关注的结论在于 1% 高值。即使关键线程自由浮动时平均值相同,其 1% 高值也比关键线程锁定到性能核心时更差。这意味着当关键线程自由浮动时,帧速率卡顿更多。最常见的原因是调度程序启发式方法基于滑动窗口运行,需要一段时间才能响应一帧所需工作量的激增。在这段时间内,关键线程可能运行在性能较低的核心上。
每个制造商都可以调整这些调度程序启发式方法,使其适合自己的硬件。例如,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:在较慢核心上运行的长杆示例。 在较慢核心上的异构工作窃取长杆示例 当长杆作业落在较快的核心上时,情况确实会好转。在这种情况下,总时间为 1,500 μs。虽然仍不理想,但已经更好了。 图 4:在较快核心上运行的长杆示例。 在较快核心上的异构工作窃取长杆示例 最佳结果是通过保持作业规模较小且分布均匀来避免长杆作业。在此示例中,总执行时间降至 1,125 μs,在完成相同工作量的情况下,仅为前一个示例时间的一半。 图 5:通过使用合适的作业时长来避免长杆作业。 使用合适作业时长的异构工作窃取 更重要的是,如果操作系统试图在作业执行过程中中断,系统会自动平衡工作负载。在这种情况下,操作系统在一个快速核心上以一个 375 μs 的任务中断了游戏。这种重新平衡对整体帧时间没有影响,帧时间仍保持在 1,125 μs。其中一个作业迁移到了慢速核心上的空闲时间中。 图 6:带有工作负载重新平衡的操作系统中断可保持稳定的帧时间。 带有中断且使用合适作业时长的异构工作窃取 这些示例仅涵盖长杆作业可以立即开始的情况;如果它要到作业队列靠后的位置才能开始,情况会更糟。这一切都归结于阿姆达尔定律,即任何系统的执行时间都受该系统中运行时间最长的部分制约。引擎不可能吸收任何会增加这些长杆线程执行时间的激增。这些激增可能来自在较慢的核心上运行,也可能来自系统中其他高优先级进程的中断。

总结

通过实施这些建议,可以确保游戏可靠运行,并在不同的 CPU 架构以及其他 Windows 进程的干扰下保持弹性。

附录

确定核心的能效级别

使用 GetLogicalProcessorInformationEx 函数确定系统中每个处理器的能效级别。遍历该函数返回的数据,查找包含能效级别和核心掩码的 RelationProcessorCore 块。

根据功耗固定线程

如前所述,由于计算机上的其他进程可能造成干扰,因此不建议在桌面环境中使用关联来固定核心。不过,Windows 提供了一些方法,使游戏可以向操作系统提供提示,建议应在何处执行工作。提供此类指导的一种方法是通过电源管理 API。

使用 CPU 集查询能效级别

使用 CPU 集是查询系统中每个核心能效级别的另一种方法。

使用 CPU 集将线程分配给能效级别

CPU 集也可用于代替显式设置线程的关联掩码。建议使用 CPU 集而不是显式关联掩码。
最后修改于 2026年10月20日