您想找什么?
Engine & platform

提高 Burst 检查器的搜索性能

JONAS REHOLT / UNITY TECHNOLOGIESStudent Software Developer
Jul 11, 2023|7 Min
提高 Burst 检查器的搜索性能
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

我叫 Jonas Reholt,是 Burst 团队的一名学生。我在博客上分享了我的优化历程,正是这些优化使 Burst 检查器最近的性能变化成为可能。Burst Inspector 的搜索速度现在提高了 13 倍,使开发人员在优化项目时能更快地专注于他们关心的代码。

继续阅读,了解如何使用 Unity Profiler 调查程序中的性能瓶颈以及如何修复它们。

Burst 检查员介绍

Unity Burst 编译器可将 C# 代码转换为高度优化的汇编代码。Burst 检查器可让您直接在 Unity 编辑器中检查该汇编代码,因此您无需使用外部工具来进行简单的代码检查。

首次打开 Burst 检查器并选择要显示的目标作业时,会看到一个类似下图的窗口。

Burst 检查器窗口显示 Burst 目标任务的已编译汇编代码
Burst 检查器窗口显示 Burst 目标任务的已编译汇编代码

如您所见,Burst 检查器提供语法高亮显示、分支流箭头等功能。

检查器会尝试滚动到执行所选目标功能的装配体,但也可以在装配体视图中搜索具体说明、注释等。这就是本博文的主题。

提高文本搜索性能

要执行搜索,检查器必须搜索原始装配输出,并将这些索引转换为检查器视图中的位置。最初的搜索功能遵循下图所示的模式,并在很大程度上依赖于System.String.IndexOf(*) 的实现。

while (assemblyCode.IndexOf(key, accIdx) >= 0) {
// ...	
// Do logic for handling search hits
// ...
}

在 135,582 行汇编代码上运行上述搜索,进行一次普通搜索命中(共 21,769 次命中),第一次搜索的执行时间约为 12 秒,后续搜索的执行时间约为 5 秒。对于 GUI 事件来说,这并不是一个理想的等待时间,所以我们必须做点什么。通过 Unity Profiler 运行搜索发现,37.3% 的执行时间花在IndexOf(*) 上,如下所示。

在 Burst 检查器中搜索普通字符串的剖面运行情况
在 Burst 检查器中搜索普通字符串的剖面运行情况

合理的优化必须解决对该函数的依赖问题,要么是定制实现,要么是完全改变算法。无论使用哪种算法,都需要步进整个字符串。因此,需要定制一些用于查找匹配项的实现方法。有鉴于此,开始优化时保留原有算法,但创建一个自定义IndexOf函数似乎是合适的。

LongTextArea.GetFragNrFromBlockIdx()耗时 3.34 秒,是因为要检索未着色的程序集代码。用于执行搜索。Burst 检查器目前会保存两次程序集代码,一次是格式化后的渲染代码,另一次是未格式化的代码。

编写自定义函数还有一个很好的副作用,就是减少了调用次数,因为目前每次搜索命中都要调用一次,外加一次。

IndexOf(*)的源代码揭示了一个强大的通用实现所需的许多安全检查。不过,就我们的情况而言,我们可以有把握地假设这些检查大部分都是正确的。为了尽量提高性能,您需要创建一个类似 C 语言的函数,以避免边界检查等问题。

您可以按照下面的伪代码编写函数,其中IsKeyMatch(*)只是检查键是否匹配。

List<int> Search(string assemblyCode, string key, int accIdx) {
     var hits = new List<int>();
     for (i = accIdx; i < assemblyCode.len - key.len; i++) {
	     if (IsKeyMatch(assemblyCode, key, i)) {
		     hits.add(i);
	     	     i += key.len-1;
          }
     }
     return hits;
}

不过,由于 C# 是托管语言,这个类似于 C 语言的功能需要您将使用的托管 Objective-C 钉住,这样垃圾回收器就不会重新定位内存地址。以下是模板代码:

unsafe {
	fixed (char* source = assemblyCode) {
		fixed (char* needle = key) {
			CustomIndexOf(source, key)
		}
	}
}

把这些东西放在一起,就能把原来的while循环分离成对索引查找器的单次调用和处理搜索命中的逻辑:

matches = FindAllMathces(text, key)
foreach match {
	...
	Do logic for handling search hits
	...
}

收获是什么?以之前的小例子为例,对代码的这一改动使首次调用的速度提高了 6.6 倍,后续调用的速度提高了 13.2 倍(以新/旧值衡量)。初始搜索速度较低的原因是,为了避免在颜色字符串中找到匹配项,需要加载未格式化的程序集。

在 Burst 检查器中搜索文本的运行时间测量结果
在 Burst 检查器中搜索文本的运行时间测量结果

有了这些改进,现在,点击量略低于 22,000 次的重载搜索,首次搜索耗时约为 1.8 秒,后续搜索耗时约为 0.4 秒。这使得 Burst 检查器更适用于大型装配,因为每次搜索时不再有足够的时间来泡一杯茶。

您现在就可以通过 Burst 1.8.7 软件包利用这一性能改进。

想了解 Burst 的更多信息?在Burst论坛与我们联系。作为Tech from the Trenches系列的一部分,请务必关注来自其他 Unity 开发人员的更多新技术博客