简要讲解何时、何处以及为何要使用 [BurstCompile]
特性
原文:When, where, and why to put [BurstCompile], with mild under-the-hood explanation
人们常对应将 [BurstCompile]
特性放在何处以及放在不同位置时有什么会被“Burst”感到困惑,所以我决定将所有的解释都写在这里。
首先要了解两件事,所有其他事情都源于这两点,但一开始似乎有些令人惊讶:
- Burst 只能对静态方法进行 Burst 编译,这就是全部内容。如果你想在下面概述的特殊情况之外实现这一点,只需将
[BurstCompile]
特性添加到静态方法及其声明类型上。当你调用这个方法时,它会自动调用经过 Burst 编译的版本,假设 Burst 编译没有失败。 - 除非你非常努力,所有从经过 Burst 编译的代码调用的 C# 代码都是经过 Burst 编译的(译注:Burst 具有传染性)。
如果上面的第一条是正确的,那么 IJob*
的 Execute
方法( IJob*
的任何值都不是静态的)和 ISystem
的 OnUpdate
方法及其朋友们(它们也不是静态的)又是怎么回事呢?
答案是,每个都有不同的魔法。
对于 Job ,每个 Job 接口都用像 [JobProducerType(typeof(SomeOtherSpecialSecretType))]
这样的属性进行注解。
在定义 IJob
接口时,添加了 [JobProducerType(typeof(IJobExtensions.JobStruct<>))]
特性。可以在此处查看示例: https://github.com/Unity-Technologies/UnityCsReference/blob/2022.2/Runtime/Jobs/Managed/IJob.cs#L36 。
如果你在实现接口的类型上添加 [BurstCompile]
特性,Burst 会知道查找接口上的这个特性,找到另一个类型(在这种情况下是 IJobExtensions.JobStruct
),确保它是泛型的(它确实是),用你给定的Job结构体类型特化它(使其成为 IJobExtensions.JobStruct<YourPersonalJob>
),然后编译另一个类型上的静态 Execute
方法。另一个类型上的静态 Execute
方法将在某种上下文中使用某些参数调用你个人的那个 Execute
方法,并且你个人的 Execute
将作为更大的静态函数的一部分进行 Burst 编译(参考前面提到的第2点)。
这也是导致有时会在 Burst 检查器看到外层循环中有不是你的代码的原因。
对于 ISystem.On*
回调,我们运行一个 ILPostProcessor ,专门生成一个静态函数,它接受 一个 void*
参数(作为 this
)和一个 ref SystemState
参数。我们将 void*
转换回特定的System类型,然后调用其非静态的 OnUpdate
方法。然后 Burst 编译器对我们的静态包装器进行 Burst 编译,而我们在更新系统时,要确保调用静态包装器。
对这一点的推论:
-
当从非 Burst 编译的代码调用具有
[BurstCompile]
特性的Job结构体的非Execute
方法时,被调用的方法不会被 Burst 编译(除非它们是静态的,并且在该方法和Job结构体上显示添加[BurstCompile]
特性)。但是,如前所述,所有从经过 Burst 编译的代码调用的 C# 代码都是经过 Burst 编译的。 -
ISystem
魔法使得看起来可以对非静态函数进行 Burst 编译,但你不能一般来说这么做。
祝你 Burst 编译愉快!
将 [BurstCompile]
放在静态方法以及声明类型上的原因是为了减少 Burst 扫描需要编译的内容时所需的时间。如果我们不要求它在类型上,它将不得不在每个类型的每个方法上查找特性,而现在的做法下,它只需检查类型本身,如果类型没有特性就继续查找其他类型。[2023年5月19日编辑:这曾经也适用于 ISystem
,但是在那里添加了一点额外的魔法,所以你只需要将特性添加到 On*
函数上,而不用添加到类型本身。额外的魔法基本上是,无论如何我们都要为代码生成处理所有的 ISystem
,所以对我们来说,检查 On*
函数是否有 [BurstCompile]
并在处理过程中为你将 [BurstCompile]
特性添加到类型上并不会带来额外的麻烦。]
[稍后编辑]
如果你担心某些特定代码是否以 Burst 方式执行,你可以使用以下模式:
[BurstDiscard]
void SetFalseIfUnBursted(ref bool val)
{
val = false;
}
bool IsBursted()
{
bool ret = true;
SetFalseIfUnBursted(ref ret);
return ret;
}
显然,根据这个函数的值改变行为是不明智的,因为当你关闭 Burst 编译时,你的代码行为会发生变化,你会立即变得抓狂。
(译注:补充内容1)
在 1.0 中, ISystem
不支持 Entities.ForEach
。话虽如此,我相信 Entities.ForEach
在 SystemBase
和 ISystem
中都默认为 Burst 编译。
ISystem
方法在 1.0 或 0.51 中都不是默认的 Burst 编译;你必须将 [BurstCompile]
放在类型和每个想要进行 Burst 编译的回调方法上,这是通过前面提到的 ILPostProcessor (ILPP) 实现的。
(译注:补充内容2)
在 1.0 版本中,我将 BurstCompatible
重命名为它实际的含义,即 GenerateTestsForBurstCompatibility
。
然而,我认为不幸的是我们不得不将其公开,因为唯一能使它得到实际执行的方法是运行一些难以找到的代码来生成测试,然后用这些测试构建一个Player版本,看看Player版本构建是否失败。
所以,我不建议使用它,除非你真的打算做所有这些繁琐的工作。如果你只是想告诉代码的读者你打算让一个方法兼容 Burst,我建议用一个注释。
对于实际上要进行 Burst 的东西, [BurstCompile]
确实是唯一重要的。