Skip to main content
CPU 制造商一直在性能、功耗和芯片面积之间取得平衡。原因是客户有不同的使用模式和优先级。桌面用户更关心性能,而笔记本用户更关心电池续航。更小的芯片面积可以带来更低的生产成本。为了满足这些相互竞争的需求,CPU 制造商设计了 异构架构,让系统能够根据当前工作负载动态调整,同时保持成本效率。 CPU 制造商通过使用不同效率等级的 CPU 核心来实现异构拓扑。这种方式用更节能的核心替换了现有 CPU 架构中的部分核心。如果用户不需要 CPU 的全部性能,操作系统会关闭高性能核心以显著节省功耗,从而延长电池续航;需要更多性能时,操作系统可以动态开启高性能核心以处理更高的负载。

硬件

不同的独立硬件供应商(IHV)对异构 CPU 拓扑的定义各不相同。Intel 和 Qualcomm 使用 PerformanceEfficiency,而 AMD 使用 PerformanceCompact。各家在设计核心拓扑时都会在性能与效率之间进行取舍。 下面的例子说明了其中涉及的一些设计选择:
  • 不同的缓存大小
    • 不同核心可能具有不同的缓存大小。
    • 例如,高性能核心可能具有更大的 L1 与 L2 缓存。
  • 缓存共享
    • 核心之间的缓存共享程度可能不同。
    • 例如,高性能核心可能各自拥有私有的 L2 缓存,而节能核心可能共享同一 L2 缓存。
  • 对对称多线程(SMT)的支持
    • 某些核心可能支持 SMT,而另一些则不支持。
    • Intel 将 SMT 称为 Hyper-Threading
  • 核心内部布局
    • 各类核心的内部资源可能不同。
    • 例如,高性能核心可能包含更多的浮点管线、ALU 管线,或更宽的数据通道。
  • 最大频率
    • 各类核心的最大频率可能不同。
    • 例如,高性能核心可能达到比节能核心更高的频率。
这些设计旨在在 CPU 内部各核心之间平衡性能与功耗。频率更高的核心具有更高的性能,但功耗也更大。多个核心共享同一缓存会缩小芯片面积,减少功耗,但同时降低性能。目标是允许在性能与功耗之间做出更灵活的动态选择。如果用户不需要满性能,操作系统就切换到更节能的核心以降低功耗。 需要注意的是,Windows 有一项要求 —— CPU 上所有核心必须支持相同的 指令集架构(ISA)。这意味着,如果一颗 CPU 支持某个特定指令集,那么其中所有核心都必须支持该指令集。例如,如果 CPU 声明支持 AVX2,则其所有核心都支持 AVX2。 关于高性能核心与节能核心之间差异的详细信息,请参见各 IHV 的官方文档。

性能数据

不同效率等级的核心性能表现自然不同。一颗核心为性能而设计,另一颗则为节能而设计。问题是它们之间的差异有多大。 以下数据是跨多家 IHV 的综合结果。每家 IHV 由于所选的拓扑不同,各测试的缩放比例也不同。虽然具体数值有差异,但整体规律在所有 IHV 中是相似的。此外,每一代 CPU 会有不同的取舍,从而调整相对数值。构建一份覆盖所有现有 IHV 和世代的数据超出本文范围。

合成测试

向量数学是游戏在一帧中所做工作的重要组成部分,可能涵盖物理、AI、渲染等。游戏会通过包含三个或四个 float(可放入一个 128 位 SSE 寄存器)的向量类型来完成这些工作。它们涵盖点积、矩阵乘法、平方根计算和向量变换等广泛的操作。这些操作的性能差异会对整体帧时产生显著影响。 下图展示了执行七种常见数学运算的相对成本。测量使用存储在单个 128 位 SSE 寄存器中的四个 32 位浮点值的向量。矩阵由四个向量组成,总共十六个 32 位浮点值,分布在四个 128 位 SSE 寄存器中。选择这些操作是因为它们在一帧内常被使用。
  • Dot product:计算向量的点积。
  • Transform vector:通过与矩阵相乘来变换向量。
  • Vector add:将两个向量相加。
  • Cross product:计算两个向量的叉积。
  • Vector sine:对向量中每个元素计算其正弦的近似值。
  • Vector FMA (fused multiply-add):两个向量相乘并加上第三个向量。
  • Inverse square root:计算向量长度的倒数平方根。
图 1:执行操作所需时间。数值越低越好。 Heterogeneous Synthetic Benchmark Numbers 在此图中,y 轴表示执行操作所需的时间。每根柱形代表所有 IHV 测得的时间范围,分别对应在高性能核心或节能核心上运行时的最短与最长时间。 如预期,不同类型核心之间的性能存在差异。然而这些数据的关键之处在于,性能差异并非在所有操作与所有 IHV 上都成比例。虽然不同 IHV 的结果差异整体相近,但某些操作的差异更大。差异更大的操作主要出现在 Vector Sin 与 Inverse Square Root 上。这种差异可以追溯到高性能核心与节能核心之间的架构选择。造成该差异的具体原因可能来自多方面。例如,更强大的核心可能拥有更多浮点管线,从而并行执行更多操作。具体原因与每个 IHV 和 CPU 世代密切相关。也可能存在某些配置下,某些操作并不表现出性能差异。总之,作品需要在不同 IHV 之间考虑这种波动性。

真实世界

之前的数据展示的是隔离环境中的特定基准测试,只反映了部分实际情况。以下数据来自一款 AAA 游戏引擎的基准测试,该引擎使用两个主线程(Simulation 与 Render)以及一组任务线程。基准测试采用三种配置。
  • 两个主线程只在高性能核心上运行。
  • 两个主线程只在节能核心上运行。
  • 两个主线程可以在系统中所有核心上运行。
在每种配置中,任务线程可以在系统中所有核心间自由浮动。渲染工作使用最低的分辨率与画质,以保证在整个测试期间作品是 CPU 瓶颈,并且让 Simulation 线程成为决定帧率的主要因素。 下图展示了一帧中各线程的执行时间。它同时展示了平均帧时与 1% 高值。1% 高值是最长的 1% 帧的阈值,只有 1% 的帧时超过该值。这个数字能很好地表示卡顿帧的数量,越偏离平均值,卡顿越严重。所有情况下,数值越低意味着长帧越少、可见卡顿越少。 如前所述,这里的数据是跨多家 IHV 的综合结果。每家 IHV 的拓扑选择会导致每项测试的缩放不同。虽然具体数值有差异,但各 IHV 的模式相似。每一代 CPU 可能进行不同权衡,从而调整相对数值。生成覆盖所有现有 CPU IHV 和世代的数据超出本文范围。 图 2:平均耗时与 1% 高值。数值越低越好。 Heterogeneous Real World Benchmark Numbers 在此图中,y 轴表示单帧所需时间。每根柱形代表所有 IHV 测得的时间范围,分别对应各线程拓扑测试的最短与最长时间。 操作系统线程调度器使用启发式规则来判断线程执行的工作量。这些启发式规则由 OS 的选择与 IHV 的驱动微调共同决定。调度器据此选择线程应在哪些核心上执行。当调度器判断某个线程执行了大量工作时,会将其迁移到更高性能的核心。 由于这些启发式规则,可能出现一种反直觉的行为:当关键线程被允许自由浮动时,作品反而运行得更快。这种情况并不总会发生,取决于工作负载、核心拓扑与具体的调度器启发式规则。 在此处测试的情境中,Simulation 线程每帧运行的时间比 Render 线程更长。调度器更倾向于将其放在更高性能的核心上。它还会将 Render 线程放到性能较低的核心上,因为它花更多时间挂起等待工作。总的效果是,这一决定为其中一个任务线程释放了一颗高性能核心。如果 Render 线程做的工作更多,这种情形可能就不会发生。 然而,这张图最有意思的地方在于 1% 高值。即便自由浮动时平均值相同,当关键线程被锁定到高性能核心时,1% 高值也比自由浮动时更好。这意味着关键线程自由浮动时帧率更容易抖动。最常见的原因是调度器启发式规则基于滑动窗口运行,响应帧工作量激增需要时间,在这段时间内关键线程可能运行在性能较低的核心上。
各家厂商可以针对自家硬件调整这些调度器启发式规则。例如 AMD 有 Workload Profile Scheduling,Intel 有 Thread Director 技术。每家 IHV 也倾向于根据 SoC 的具体拓扑对启发式规则进行调整。

建议

线程亲和性

对于只有单一效率等级的桌面机器,建议是避免设置硬性线程亲和性,而应让线程在所有核心间自由浮动。主要原因是,你事先并不知道用户机器上还运行着哪些其他进程,一个具有硬亲和性的线程可能被安排到与系统中另一个高优先级线程共享的核心上,结果就是作品线程被“饿死”。如果这种争用发生在帧关键线程上,作品会出现帧时不一致。你希望操作系统始终有地方可以把作品线程迁移,以保证它们能继续运行。 但在异构 CPU 上,这一建议需要改变。主要原因是操作系统不一定知道作品的哪些线程或任务对帧关键。它有一些启发式规则来判断,但仍可能做出错误决定,可能把这些线程或任务迁到性能较低的核心上,从而增加作品出现性能问题的概率。 建议目标是允许作品线程至少在三到四颗核心上运行,越多越好。例如,在低端系统上,如果只有一颗高性能核心和三颗低性能核心,就应该让线程在所有核心间自由浮动;在拥有三颗或更多高性能核心的机器上,你可以将帧关键线程限制在高性能核心上。 在拥有大量核心且支持 SMT 的高端机器上,一些作品可能会试图将线程锁定到物理核心,以避免几个高优先级线程共享同一物理核心资源导致的争用。总体来说,并不建议为此做特殊处理,OS 已经在系统范围内实施了此行为 —— 先将线程分配到不同物理核心,只在必要时才使用更多逻辑核心。 作品只有在系统中拥有足够高性能核心以支持其关键线程时才应考虑此做法。此时,可能希望将帧关键线程锁定到高性能核心。不过,这些线程仍应允许在所有高性能核心之间自由浮动。虽然在某些情况下允许所有线程在所有核心间自由浮动能提升性能,但这一行为在不同 CPU 制造商之间并不一致。 如果作品实现了一个能在所有线程间完全负载均衡的任务窃取(work stealing)系统,则任何任务线程都应允许在所有核心间自由浮动,无论其属于高优先级还是低优先级任务线程。任务窃取算法会自动均衡负载,让所有工作在最短时间内完成。高性能核心上的线程在一帧中执行更多任务。 如果有多个帧关键线程,可以设置它们的亲和性,使得两两不会运行在同一颗核心上。例如,若有两个帧关键线程,则把它们的亲和性设为彼此的异或(XOR),就能避免 OS 让它们相互争用。同样,只有在系统有足够核心的情况下才考虑此做法,同时仍要允许每个帧关键线程至少在三到四颗核心间浮动。

线程拓扑

在异构环境中规划如何将工作分配到线程时,主要数据围绕各任务的时长以及它们之间的交互。每个任务的平均耗时是多少、任务之间有哪些依赖关系、各任务的优先级如何等等。 任务线程能在多大程度上吸收关键线程或任务的耗时波动?如果其中一个线程或任务多花 35% 的时间,会对帧时产生多大影响?例如,一个任务运行超时会对本帧的其余工作造成多大延迟,系统是否会停下等它完成? 在关键线程或任务中人为增加额外开销,以此评估异构系统对帧时的影响,通常很有帮助。作为测试,随机让一个任务或工作块多花 25%–35% 的时间。整体帧时的变化程度反映了任务系统吸收执行波动的能力。理想情形是帧时几乎没有变化,说明系统能吸收任意的帧时波动。这些波动既可能来自运行在性能较低的核心上,也可能来自系统中其他进程占用 CPU。

线程通信

使用锁原语进行线程通信时需要谨慎考虑。上下文切换的开销与新线程开始运行前接收方核心的 C-State 密切相关。核心必须处于 C0(运行状态)后,新线程才能开始运行。最便宜的情形是核心已经在运行,已在 C0;然而如果核心空闲,它会处于更低的 C-State。C-State 越低,睡眠越深,功耗越低,但唤醒与恢复运行的代价也更高。最常见的低功耗 C 状态是 C1,可能增加 2–7 μs 的额外开销;下一个 C2 可能增加约 40–100 μs。具体数字与 IHV 与 SoC 有关。有很多变量影响核心空闲时处于哪个 C-State,是响应速度与功耗之间的平衡。较低的 C-State 比更高的 C-State 更省电。常见的做法是核心先进入 C1,如果一段时间未运行代码则切换到 C2。 还有另一个问题 —— 提交任务的线程与执行任务的线程分处不同性能特征的核心时会发生什么。技术上讲,任务线程有可能比新任务被创建的速度还快地完成任务。这种情况在任务线程运行在比任务提交线程更快的核心上时更容易发生。 如果一个任务的执行时间比创建它所需的时间还短,作品就可能出现任务线程不断挂起等待新任务的情形。总的效果是任务执行成本上升 —— 上下文切换的开销被加到每个任务的执行时间上。这个开销可能会矛盾地导致在更高性能的核心上运行时帧率反而更低。例如,原本 250 μs 的工作可能突然变成 275 μs,这就是 10% 的性能下降。 针对这种情况,建议不要一边创建一边提交任务,而是批量提交。例如,如果系统的模式是构建一个任务、提交一个任务,再构建一个任务、提交一个任务……,应改为先批量构建,然后一次性全部提交。 如果作品使用了要求 WaitForSingleObject 的对象,应将其放入一个小的自旋循环中。先以超时 0 调用该函数,自旋结束后再使用作品的正常超时。不过,更好的解决办法是使用现代同步 API,例如 Slim Reader/Writer (SRW) 锁临界区WaitOnAddress。这些同步 API 内部都包含某种形式的自旋,可以避免线程不断挂起。对于持有时间较短的锁尤其有效。 务必避免长时间的自旋锁,例如让线程不断自旋等待工作的模式。如果核心上有其他工作需要执行,调度器最终会挂起自旋线程去执行其他工作。当发生这种抢占时,被挂起线程很可能被挂起较长时间。这是为了防止其他线程被饿死。由于作品对何时被挂起几乎没有控制权,这会导致帧率抖动。

任务系统

为获得最佳性能,应围绕任务而非依赖长期存在的帧关键线程来组织引擎。例如,Simulation/Render 线程模型创建了两个持久的帧关键线程。然而,一个耗时较长的任务会让任何任务线程表现得像帧关键线程:如果一个任务耗时 10 ms,那么在这段时间里,执行它的线程就成了帧关键线程,与其原本角色无关。

任务窃取(Work Stealing)

首要建议是在整个任务系统中采用任务窃取,并让任何帧关键线程也参与到任务窃取算法中。例如,如果渲染线程正在等待与渲染相关的任务完成,它应该执行其中一部分任务。 许多游戏引擎已经在实践中使用了这种做法。这里之所以特别提及,是因为这种行为对于引擎吸收由异构核心和可能的 OS 抢占引起的帧时波动至关重要。 任务执行时间变长时,帧时波动的风险会上升。这种波动可能来自任务运行在性能较低的核心上,或来自 PC 上的其他工作。建议引擎将任务耗时控制在约帧时的 2% 左右。例如,对于 60 fps 的游戏,大约不超过 333 μs。如有必要,可以将多个较小任务合并为一个较大任务以达到 2% 的目标。这种做法在任务大小、线程通信与吸收波动能力之间提供了良好平衡。 以下示例展示了长耗时的帧关键任务对系统性能的影响。示例中每个任务包含相同数量的操作。示例中的一个任务在慢核上耗时 375 μs,在快核上耗时 250 μs。长耗时任务比其他任务贵 6 倍。 最坏情况是长耗时任务被安排到慢核上,可能对帧时产生严重影响。此情形下总工作时间为 2,250 μs(6 × 375 μs)。 图 3:长耗时任务运行在慢核上的示例。 Heterogeneous Work Stealing long pole example on a slower core 当长耗时任务被安排到快核上时,情况会好一些,此时总时间为 1,500 μs。仍不算好,但更好。 图 4:长耗时任务运行在快核上的示例。 Heterogeneous Work Stealing long pole example on a faster core 最佳做法是通过保持任务大小小而均匀来避免长耗时任务。在这个示例中,总执行时间降到 1,125 μs,是前一个示例的一半,同样完成了相同的工作量。 图 5:通过合理的任务长度避免长耗时任务。 Heterogeneous Work Stealing with proper job length 更重要的是,如果 OS 在任务执行过程中试图打断,系统会自动重新均衡负载。此例中,OS 在快核上以 375 μs 的任务打断了作品。这次再均衡对整体帧时没有影响,仍保持 1,125 μs。其中一个任务被迁移到慢核上以填补空闲时间。 图 6:OS 打断时通过负载再均衡保持稳定帧时。 Heterogeneous Work Stealing with proper job length with interruption 这些示例仅涵盖长耗时任务可以立即开始的情形,若它必须等待队列中的其他任务之后才能开始,情况会更糟。这一切最终归结为 Amdahl 定律 —— 任何系统的执行时间都由其中运行时间最长的部分决定。引擎无法吸收那些长耗时线程执行时间的任何波动,而这些波动可能来自运行在慢核上,或来自系统中其他高优先级进程的打断。

总结

按上述建议实施后,你可以确保游戏在不同的 CPU 架构下、面对来自其他 Windows 进程的干扰时仍能可靠地运行并保持韧性。

附录

判断核心的效率等级

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

基于功耗的线程绑定

如前所述,在桌面环境中由于可能受到其他进程的干扰,不建议通过亲和性进行核心绑定。但 Windows 提供了让作品向 OS 提供提示的方式,推荐工作应在哪些位置执行。其中一种方式是通过电源管理 API。

使用 CPU Sets 查询效率等级

使用 CPU Sets 是查询系统中每个核心效率等级的另一种方法。

使用 CPU Sets 将线程分配到效率等级

CPU Sets 也可以替代显式设置线程的亲和性掩码。建议使用 CPU Sets 而非显式的亲和性掩码。
最后修改于 2026年8月13日