CPU 优化

测量性能

我们必须知道“瓶颈”在哪里,才能知道如何加快我们的程序。瓶颈是指程序中最慢的部分,限制了所有事情的进展速度。专注于瓶颈,可以让我们集中精力优化能给我们带来最大速度提升的地方,而不是花大量时间去优化那些能带来微小性能提升的功能。

对于 CPU 来说,找出瓶颈的最简单方法就是使用性能剖析器。

CPU 分析器

剖析器与您的程序一起运行, 并进行时间测量, 以计算出每个功能所花费的时间比例.

Godot集成开发环境有一个方便的内置剖析器. 它不会在每次启动项目时运行: 必须手动启动和停止. 这是因为, 与大多数剖析器一样, 记录这些时序测量会大大减慢你的项目速度.

剖析后, 你可以回看一帧的结果.

../../_images/godot_profiler.png

Godot 性能分析器的截图

其中一个演示项目的简介结果.

备注

我们可以看到物理, 音频等内置流程的消耗, 也可以在底部看到自己脚本功能的消耗.

等待各种内置服务器的时间可能不会被计算在剖析器中. 这是一个已知的错误.

当一个项目运行缓慢时, 你经常会看到一个明显的功能或流程比其他功能或流程花费更多的时间. 这是你的主要瓶颈, 你通常可以通过优化这个领域来提高速度.

有关使用Godot内置分析器的更多信息, 请参阅: 调试器面板.

外部分析器

虽然Godot IDE剖析器非常方便有用, 但有时你需要更强大的功能, 以及对Godot引擎源代码本身进行剖析的能力.

你可以使用一些第三方分析器来完成这个任务, 包括 Valgrind , VerySleepy , HotSpot , Visual StudioIntel VTune .

备注

您需要从源码编译Godot以使用第三方剖析器. 这是获得调试符号所必需的. 你也可以使用调试构建, 但是, 请注意, 剖析调试构建的结果将与发布构建不同, 因为调试构建的优化程度较低. 在调试构建中, 瓶颈往往在不同的地方, 所以你应该尽可能地对发布构建进行剖析.

Callgrind 的截图

例子结果来自Callgrind, 这是Valgrind的一部分.

从左边开始,Callgrind正在列出函数及其子函数内的时间百分比(Inclusive), 函数本身(不包括子函数)内的时间百分比(Self), 函数被调用的次数, 函数名称以及文件或模块.

在这个例子中,我们可以看到几乎所有的时间都花在 Main::iter() 函数下。这是 Godot 源代码中被反复调用的主函数。它导致帧被绘制、物理学 ticks 被模拟、节点和脚本被更新。很大一部分时间是花在渲染画布的函数中(66%),因为这个例子使用的是 2D 基准。下面,我们看到几乎 50% 的时间都花在了 Godot 代码之外的 libglapii965_dri(图形驱动)中。这告诉我们,很大一部分 CPU 时间都花在了图形驱动上。

这其实是一个很好的例子, 因为在理想的世界里, 只有很小一部分时间会花在图形驱动上. 这说明存在一个问题, 就是在图形API中进行了太多的交流和工作. 这种特殊的剖析导致了2D批处理的发展, 通过减少这方面的瓶颈, 大大加快了2D渲染的速度.

手动计时函数

另一个方便的技术, 特别是当你使用分析器确定了瓶颈后, 就是手动为功能或被测区域计时. 具体细节因语言而异, 但在GDScript中, 你可以做如下操作:

  1. var time_start = OS.get_ticks_usec()
  2. # Your function you want to time
  3. update_enemies()
  4. var time_end = OS.get_ticks_usec()
  5. print("update_enemies() took %d microseconds" % time_end - time_start)

当手动为函数计时时, 通常最好是多次(1000次或更多次)运行该函数, 而不是只运行一次(除非是非常慢的函数). 这样做的原因是, 定时器的精度往往有限. 此外,CPU会以一种无序的方式调度进程. 因此, 一系列运行的平均值比单次测量更准确.

当你尝试优化功能时, 一定要反复对它们进行剖析或计时. 这将为您提供关键的反馈, 说明优化是否有效(或无效).

缓存

CPU缓存是另外一个需要特别注意的东西, 特别是在比较一个函数的两个不同版本的时序结果时. 其结果可能高度依赖于数据是否在CPU缓存中.CPU不会直接从系统RAM中加载数据, 尽管它与CPU缓存相比非常巨大(几千兆字节而不是几兆字节). 这是因为系统RAM的访问速度非常慢. 相反,CPU从一个较小, 较快的内存库中加载数据, 称为cache. 从缓存中加载数据的速度非常快, 但每次你试图加载一个没有存储在缓存中的内存地址时, 缓存必须前往主内存并缓慢地加载一些数据. 这种延迟会导致CPU长时间闲置, 被称为 “cache miss”.

这意味着, 第一次运行一个函数时, 由于数据不在CPU缓存中, 它可能运行得很慢. 第二次和以后的时间, 可能运行得更快, 因为数据在缓存中. 由于这个原因, 在计时时一定要使用平均数, 并且要注意缓存的影响.

了解缓存对于CPU优化也是至关重要的. 如果你有一个算法(例程), 从主内存随机分布的区域加载小数据位, 这可能会导致大量的缓存失误, 很多时候,CPU会在附近等待数据, 而不是做别的工作. 相反, 如果你能使你的数据访问本地化, 或者更好的是以线性方式访问内存(像一个连续的列表), 那么缓存将以最佳方式工作,CPU将能够尽可能快地工作.

Godot通常会为你处理这些低级的细节. 例如, 服务器API确保数据已经为渲染和物理学等方面的缓存进行了优化. 不过, 在使用 GDNative 时, 你还是要特别注意缓存问题.

语言

Godot支持多种不同的语言, 值得注意的是, 其中有一些折衷. 有些语言是以速度为代价而设计的, 便于使用, 而另一些语言速度更快, 但更难使用.

无论你选择哪种脚本语言, 内置的引擎函数都以同样的速度运行. 如果你的项目在自己的代码中进行了大量的计算, 可以考虑将这些计算转移到更快的语言中.

GDScript

GDScript 被设计成易于使用和迭代的语言, 是制作多种类型游戏的理想选择. 然而, 在这种语言中, 易用性被认为比性能更重要. 如果您需要进行繁重的计算, 请考虑将您的一些项目转移到其他语言中.

C

C # 很受欢迎, 在Godot中得到了一流的支持. 它在速度和易用性之间提供了一个很好的折中. 不过要注意游戏过程中可能出现的垃圾收集暂停和泄漏. 解决垃圾收集问题的一个常见方法是使用 对象池, 这不在本指南的范围内.

其他语言

第三方提供对其他几种语言的支持,包括 RustJavascript

C++

Godot是用C++编写的. 使用C++通常会带来最快的代码. 然而, 在实际操作层面上, 它是最难在不同平台上部署到终端用户的机器上的. 使用C++的选项包括 GDNativecustom modules .

线程

在进行大量的计算时, 考虑使用线程, 这些计算可以相互并行运行. 现代CPU有多个核心, 每个核心能做的工作量有限. 通过将工作分散在多个线程上, 你可以进一步向CPU的峰值效率迈进.

线程的缺点是,你必须非常小心。由于每个 CPU 核心都是独立运行的,它们最终可能会在同一时间试图访问相同的内存。一个线程可以在另一个线程在写的时候读取一个变量:这被称为竞态条件。在你使用线程之前,请确保你了解这些危险以及如何尝试和防止这些竞态条件。

线程也会使调试的难度大大增加.GDScript调试器还不支持在线程中设置断点.

有关线程的更多信息, 请参见 使用多线程.

SceneTree

虽然节点是一个非常强大和通用的概念, 但请注意, 每个节点都是有代价的. 内置的函数, 如 _process() 和 _physics_process() 会在树中传播. 当你有非常多的节点(通常是成千上万的节点)时, 这种内务管理会降低性能.

在Godot渲染器中, 每个节点都是单独处理的. 因此, 较少的节点数量与较多的每个节点可以带来更好的性能.

SceneTree 的一个怪癖是, 你有时可以通过从SceneTree中删除节点, 而不是通过暂停或隐藏节点来获得更好的性能. 您不一定要删除一个分离的节点. 例如, 您可以保留一个节点的引用, 使用 Node.remove_child(node) 将其从场景树中分离出来, 然后使用 Node.add_child(node) 将其重新连接. 例如, 这对于在游戏中添加和删除区域是非常有用的.

你可以通过使用服务器API来完全避免使用SceneTree. 更多信息, 请参见 利用服务器进行优化 .

物理学

在某些情况下, 物理学最终会成为一个瓶颈. 尤其是在复杂的世界和大量物理对象的情况下, 更是如此.

以下是一些加速物理的技巧:

  • 尝试使用简化版本的渲染几何图形来处理碰撞形状. 通常情况下, 这对终端用户来说并不明显, 但可以大大提高性能.

  • 试着从物理学中移除物体, 当它们不在视野中/在当前区域之外时, 或者重新使用物理学对象(例如, 也许你允许每个区域有8个怪物, 并重新使用这些怪物).

物理的另一个关键方面是物理时钟滴答率. 在一些游戏中, 你可以大大降低时钟滴答率, 比如说, 你可以不用每秒更新物理60次, 而只需每秒更新30次甚至20次. 这样可以大大降低CPU的负载.

改变物理学tick rate的缺点是, 当物理学更新速率与每秒渲染的帧数不匹配时, 你可能会出现运动抖动或抖动. 另外, 降低物理学tick率会增加输入滞后. 建议在大多数以玩家实时移动为特色的游戏中, 坚持使用默认的物理学tick率(60 Hz).

解决抖动的方法是使用 固定时间步长插值 , 这涉及到平滑多个帧的渲染位置和旋转, 以匹配物理. 你可以自己实现, 或者使用 第三方插件 . 从性能上来说, 与运行物理时钟滴答相比, 插值是一个非常廉价的操作. 它的速度快了好几个数量级, 所以这在减少抖动的同时也带来了部分显著的性能提升.