Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

CPU 优化

测量性能

我们必须知道“瓶颈”在哪里,才能知道如何加速程序。瓶颈是程序中最慢的部分,限制了整体的运行速度。专注于瓶颈,使我们能够集中精力优化那些能带来最大速度提升的领域,而不是花费大量时间去优化那些只会带来微小性能提升的函数。

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

CPU 分析器

分析器与你的程序一起运行,并进行时间测量,以计算出每个函数所花费的时间比例。

Godot IDE 方便地内置了一个分析器。它不会在每次启动项目时自动运行,必须手动启动和停止。这是因为,与大多数分析器一样,记录这些时间测量数据会大大减慢你的项目速度。

分析结束后,你可以回看某一帧的结果。

Godot 分析器的截图

其中一个演示项目的分析结果。

备注

我们可以看到物理、音频等内置进程的开销,也可以在底部看到我们自己的脚本函数的开销。

等待各种内置服务器的时间可能不会被计算在分析器中。这是一个已知的漏洞。

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

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

外部分析器

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

你可以使用若干个第三方 C++ 分析器来实现这一点。

Callgrind 的截图

来自 Callgrind(Valgrind 的一部分)的示例结果。

从左到右,Callgrind 列出了函数及其子函数的时间百分比(Inclusive)、函数本身(不包括子函数)的时间百分比(Self)、函数被调用的次数、函数名称以及文件或模块。

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

这实际上是一个很好的示例,因为在理想情况下,只有很小一部分时间会花在图形驱动程序上。这说明示例存在图形 API 通信和工作量过多的问题。恰是这个分析结果促使了 2D 批处理的开发,它通过减少这方面的瓶颈大大加快了 2D 渲染的速度。

手动计时函数

另一个实用的技巧,特别是当你使用分析器确定了瓶颈后可以使用,就是手动为待测试的函数或区域计时。具体细节因语言而异,但在 GDScript 中,你可以这样做:

var time_start = Time.get_ticks_usec()

# Your function you want to time
update_enemies()

var time_end = Time.get_ticks_usec()
print("update_enemies() took %d microseconds" % (time_end - time_start))

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

当你尝试优化函数时,一定要反复对它们进行分析或计时。这将为你提供关键的反馈,告诉你优化是否有效。

缓存

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

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

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

Godot 通常会为你处理这些底层细节。例如,服务器 API 会确保数据已经针对渲染和物理等方面的缓存进行了优化。不过在编写 GDExtension 时,你还是需要格外留意缓存问题。

语言

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

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

GDScript

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

C#

C# 很受欢迎,并且在 Godot 中拥有一流的支持。它在速度和易用性之间提供了一个良好的平衡。不过,请注意游戏过程中可能出现的垃圾回收暂停和泄漏。解决垃圾回收问题的一种常见方法是使用对象池,但这超出了本指南的范围。

其他语言

第三方提供了对包含 Rust 在内的其他语言的支持。

C++

Godot 是用 C++ 编写的。使用 C++ 通常会产生最快的代码。但在实际操作层面,C++ 是最难在不同平台上部署到终端用户的机器上的。使用 C++ 的选项包括 GDExtension 和自定义模块

线程

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

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

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

SceneTree

虽然节点是一个非常强大且用途广泛的概念,但请注意:是节点就会有开销。_process()_physics_process() 等内置函数会在场景树上传播调用。当你有非常多的节点时,这种内务管理就会降低性能(确切的数量取决于目标平台,可能从数千到数万不等,因此请确保在开发过程中评测所有目标平台上的性能)。

在 Godot 渲染器中,每个节点都是单独处理的。因此,减少节点的数量、让每个节点多做一些工作,可以获得更好的性能。

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

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

物理

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

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

  • 尝试使用简化的几何形体作为碰撞形状。虽然在通常情况下这对终端用户来说并不明显,但有时可以大大提高性能。

  • 尝试在物体离开视野/当前区域时移除其物理效果,或者重用物理对象(例如,你允许每个区域最多有 8 个怪物,并重复使用这些怪物的物理对象)。

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

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

解决抖动的方法是使用固定时间步长插值,该技术通过在多个帧上平滑渲染位置和旋转来匹配物理。Godot 有内置的物理插值功能,你可以在此阅读相关内容。从性能上来说,与运行物理步相比,插值是一个非常低成本的操作。它的速度快了几个数量级,因此这可以在减少抖动的同时带来显著的性能提升。