了解 Apple Silicon 游戏开发的 CPU 作业调度

57805-118350-gcd1-xl

对 GPU 和 CPU 的需求是现代计算机上计算最密集的工作负载之一。每帧必须处理成百上千个 GPU 作业。

为了让你的游戏尽可能高效地在 Apple Silicon 上运行,你需要优化你的代码。最大效率是这里的游戏名称。

Apple Silicon 推出了新的内置 GPU 和 RAM,可实现快速访问和性能。Apple Fabric 是 M1-M3 架构的一个方面,它允许访问 CPU、GPU 和统一内存,而无需将内存复制到其他商店,从而提高了性能。

核心

每个 Apple Silicon CPU 都包含效率核心和性能核心。效率内核设计为在极低功耗模式下工作,而性能内核则用于尽可能快地执行代码。

线程,即代码执行的路径,由调度程序在两种类型的实时线程内核上自动运行。开发人员可以控制线程何时运行或不运行,并可以使它们进入睡眠状态或唤醒它们。

在运行时,多个软件层可以与一个或多个 CPU 内核交互以编排程序执行。

这些包括:

  1. XNU 内核和调度程序
  2. Mach 微内核核心
  3. 执行调度程序
  4. POSIX 可移植 UNIX 操作系统层
  5. Grand Central Dispatch,或 GCD(基于的 Apple 专用线程技术))
  6. NSObjects
  7. 应用层

NSObjects 是由 NeXTStep 操作系统定义的核心代码对象,Apple 在 1997 年收购了史蒂夫·乔布斯 (Steve Jobs) 的第二家公司 NeXT 时收购了该操作系统。

GCD 块通过执行一段代码来工作,这些代码在完成后使用回调或闭包来完成其工作并提供一些结果。

POSIX 包括 pthreads,它们是代码执行的独立路径。Apple 的 NSThread 对象是一个多线程类,它包括 pthreads 以及其他一些调度信息。可以使用 NSThreads 及其表亲类 NSTask 来计划要在 CPU 内核上运行的任务。

所有这些层协同工作,为操作系统和应用程序提供软件执行。

指引

在开发游戏时,您需要牢记几件事才能实现最佳性能。

首先,您的总体设计目标应该是减轻 CPU 内核和 GPU 上的工作负载。运行速度最快的代码是永远不必执行的代码。

减少代码并最大化执行调度对于保持游戏平稳运行至关重要。

Apple 提供了一些建议,您可以遵循这些建议,以获得最大的 CPU 效率。这些准则也适用于基于 Intel 的 Mac。

空闲时间和调度

首先,当特定的 GPU 内核未被使用时,它会处于空闲状态。唤醒使用时,有一点点唤醒时间,成本不大。苹果是这样展示的:

57805-118146-Screenshot-9-xl

接下来,还有第二种类型的成本,即调度。当内核唤醒时,操作系统调度程序需要少量时间来决定在哪个内核上运行任务,然后它必须在内核上调度代码执行并开始执行。

信号量或线程信号也必须设置和同步,这需要少量时间。

第三,当调度程序确定哪些内核已经在执行任务,哪些内核可用于新任务时,存在一些同步延迟。

所有这些设置成本都会影响游戏的性能。在执行过程中的数百万次迭代中,这些小成本可能会累积起来并影响整体性能。

您可以使用 Apple Instruments App 来发现和跟踪这些成本如何影响运行时性能。Apple 在 Instruments 中展示了一个运行游戏的示例,如下所示:

57805-118360-Screenshot-10-xl

在此示例中,启动/等待线程模式出现在同一个 CPU 内核上。这些任务可以在多个内核上并行运行,以获得更好的性能。

这种并行性的损失是由极短的代码执行时间引起的,在某些情况下,代码执行时间几乎与单核 CPU 唤醒时间一样短。如果这个短代码的执行可以延迟一点,它可能会在另一个内核上运行,这将导致执行运行得更快。

若要解决此问题,Apple 建议使用正确的作业计划粒度。也就是说,将极小的作业分组到较大的作业中,以便集体执行时间不会接近或超过核心唤醒和计划开销时间。

每当线程运行时,总是存在很小的线程调度成本。在一个线程中同时运行多个小任务可以消除与线程调度相关的一些调度程序开销,因为它可以减少整体线程调度计数。

接下来,在计划执行作业之前,立即准备好运行大多数作业。每当启动线程调度时,通常其中一些线程会运行,但如果它们必须等待调度执行,其中一些线程最终可能会被移出核心。

当线程移出核心时,它会产生线程阻塞。通常,对线程发出信号和等待可能会导致性能降低。

反复唤醒和暂停线程可能是性能问题。

并行化嵌套的 for 循环

在嵌套循环执行期间,以较粗的粒度调度外部循环(即运行频率较低)可以使循环的内部部分不中断。这可以提高整体性能。

这还可以减少 CPU 缓存延迟并减少线程同步点。

作业池和内核

Apple 还建议使用作业池来利用工作线程来获得更好的性能。

工作线程通常是一个后台线程,它代表另一个线程(通常称为任务线程)执行某些工作,或者代表应用程序的某些更高级别的部分执行操作系统本身。

工作线程可以来自软件的不同部分。有些工作线程可以进入睡眠状态,因此它们不会主动运行或计划运行。

在作业池中,工作线程从其他线程窃取作业调度。由于所有线程都存在一些线程调度成本,因此在用户空间中启动作业比在调度程序运行的操作系统内核空间中启动作业要便宜得多。

这消除了内核中的调度开销。

操作系统内核是操作系统的核心,大多数后台和低级工作都发生在其中。用户空间是大多数应用或游戏代码执行实际运行的地方,包括工作线程。

在用户空间中使用作业窃取可以跳过内核调度开销,从而提高性能。请记住 – 最快的代码段是永远不必运行的代码段。

避免发出信号和等待

当您重用现有作业而不是创建新作业时 – 通过重用线程或任务指针,您将在活动核心上使用已处于活动状态的线程。这也减少了作业计划开销。

此外,请确保仅在需要时唤醒工作线程。确保有足够的工作准备就绪,以证明唤醒线程以运行它。

CPU 周期

接下来,您需要优化 CPU 周期,以便在运行时不会浪费任何 CPU 周期。

为此,首先要避免将线程从 E 核提升到 P 核。E 核运行速度较慢,以节省电量和电池寿命。

您可以通过避免垄断 CPU 内核的繁忙等待周期来做到这一点。如果调度程序必须在一个繁忙的内核上等待太长时间,它可能会将任务转移到另一个内核 – 如果这是唯一可用的 E 内核。

和 调度调用确定线程的运行优先级,以及何时让位于其他任务。yieldsetpri()

在 Apple 平台上使用可以有效地告诉内核让位于系统上运行的任何其他线程。这种松散定义的行为可能会造成性能瓶颈,这些瓶颈在 Instruments 的运行时很难追踪。yield

yield性能因平台和操作系统而异,并可能导致较长的执行延迟 – 长达 10 毫秒。尽可能避免使用 或,因为这样做可能会暂时将给定 CPU 内核的执行暂时归零。yieldsetpri()

另外,避免使用 – 因为在 Apple 平台上,它没有任何意义,并且是无操作。sleep(0)

扩展线程数

通常,您希望对 CPU 内核数使用正确的线程数。运行过多内核数较少的设备线程会降低性能。

过多的线程会创建成本高昂的核心上下文切换。

线程太少会导致相反的问题:在多个内核上并行调度线程的机会太少。

始终在游戏启动时查询 CPU 设计,以了解您运行的 CPU 环境类型以及可用的内核数量。

线程池应始终根据 CPU 核心数进行缩放,而不是按整体任务线程数进行缩放。

即使您的游戏设计需要大量工作线程来执行给定任务,但如果线程太多而内核太少而无法同时运行它们,它也永远无法高效运行。

您可以使用 UNIX 函数查询 iOS 或 macOS 设备。该参数返回有关设备具有的常规 CPU 内核数的信息。sysctlbynamehw.nperflevelssysctlbyname

使用仪器

在 Apple 的 Instruments 应用程序中,有一个游戏性能模板,可用于在运行时查看和衡量游戏性能

57805-118147-Screenshot-12-xl
Apple 的仪器。

Instruments 中还有一个线程状态跟踪功能,可用于跟踪线程执行和等待状态。您可以使用 TST 来跟踪哪些线程处于空闲状态以及空闲了多长时间。

总结

游戏优化是一个非常复杂的话题,我们几乎没有涉及一些可用于最大限度地提高应用性能的技术。还有很多东西要学 – 准备好花几天时间掌握这个主题。

在许多情况下,通过使用 Instruments 跟踪代码的行为方式并在出现任何性能瓶颈时对其进行修改,您将从反复试验中学到最好的知识。

总体而言,在多核 Apple 系统上进行游戏作业调度时要牢记的关键点是:

  1. 使任务尽可能小
  2. 在单个线程中对尽可能多的微小任务进行分组
  3. 尽可能减少线程开销、调度和同步
  4. 避免内核空闲/唤醒周期
  5. 避免线程上下文切换
  6. 使用作业池
  7. 仅在需要时唤醒线程
  8. 避免使用 sleep(0) 并尽可能让步
  9. 使用信号量进行线程信号
  10. 将线程数扩展到 CPU 核心数
  11. 使用仪器

未经允许不得转载:表盘吧 » 了解 Apple Silicon 游戏开发的 CPU 作业调度