Unity Burst编译器将你的C#代码转换为高度优化的机器代码。我们经常从我们的论坛用户(如@dreamingimlatios)那里得到一个问题,那就是Burst代码中的函数参数。开发者是否应该使用它们,在哪里使用?我们已经把这篇文章放在一起,试图更详细地解释它们。
什么是in 参数
C# 7.2 引入了 in 参数修饰符,即通过引用将一些东西传递给一个函数,而被调用的函数不允许修改这些数据。
int Foo(in int a, ref int b)
{
a = 42; // 这将是一个编译错误!
b = a; // 这就没有问题,因为 b 是通过引用传递的.
return a;
}
in参数是一个非常有用的语言概念,因为它在开发者和编译器之间执行了一个关于如何使用和修改数据的合同。in参数修改器允许参数以引用的方式传递,被调用的函数不允许修改数据。它与out参数修饰符(参数必须由函数修改)和ref参数修饰符(参数值可以被修改)配对。
间接参数和 ABI
让我们看一下以下简单的Job:
[BurstCompile]
public struct MyJob : IJob
{
public struct SomeStruct
{
public float3 Position;
public float4x4 Rotation;
}
public SomeStruct InDataA;
public SomeStruct InDataB;
public float3 OutData;
[MethodImpl(MethodImplOptions.NoInlining)]
private float3 DoSomething(SomeStruct a, SomeStruct b)
{
return math.rotate(a.Rotation, a.Position) +
math.rotate(b.Rotation, b.Position);
}
public unsafe void Execute()
{
OutData = DoSomething(InDataA, InDataB);
}
}
上面的代码可以分解为:
- 调用 DoSomething 方法,该方法接受两个按值传递的结构。
- 它对数据执行一些操作,然后返回结果(对于本演示而言,操作并不重要)。
- 请注意,我们已将 [MethodImpl(MethodImplOptions.NoInlining)] 放在 DoSomething 方法上。我们这样做有两个原因:
- 它可以让我们使用Burst检查器精确地指出所产生的装配中的具体方法。
- 它让我们模拟如果 DoSomething 方法真的是一个 Burst 无论如何都不会内联的非常大的函数会发生什么。
现在,如果我们启动 Burst Inspector,我们可以开始深入研究编译器为上述代码实际生成的内容:
注意我们在红框中突出显示的程序集–这是函数所需的堆栈字节数。现在是 "执行 "方法本身。
再次注意高亮的红色矩形区域–这是在寄存器rax中的一些内存地址和rsp中的堆栈之间做了大量的拷贝。那么,你可能会问,为什么它要这样做呢?
欢迎来到ABI的奇妙世界–应用二进制接口。在很久很久以前,当计算机比大多数现代房屋还大的时候,一些聪明的计算机人员意识到,如果两个不同的人要编写程序,使这两个人的代码可以一起使用,他们就必须就这样做的规则达成一致。
当数据从调用者传递给被调用者时,使用一个函数,编译器必须同意函数参数的位置,这样调用者就知道把数据放在哪里,被调用者也知道从哪里检索数据。
从一个函数向另一个函数传递数据有一套规则,调用者和被调用者都必须理解这些规则,这样他们就能在正确的位置理解正确的数据。在这种情况下,这些规则被称为调用约定,而且有很多奇怪和奇妙的种类。每个操作系统往往有不同的约定,有些操作系统有多个约定,但重要的是,双方都要遵循相同的规则,不要以你意想不到的方式行事!"。
大多数调用约定允许简单的数据(原始类型或小结构)以值和寄存器的形式传递–这是传递数据的最有效方式。但是大的结构,任何超过16字节大小的结构,一般都必须间接传递。如果我们再看一下上面展示的简单工作,我们现在已经对它进行了修改,向你展示了编译器为符合ABI而对代码所做的工作。
[BurstCompile]
public struct MyJob : IJob
{
public struct SomeStruct
{
public float3 Position;
public float4x4 Rotation;
}
public SomeStruct InDataA;
public SomeStruct InDataB;
public float3 OutData;
[MethodImpl(MethodImplOptions.NoInlining)]
private float3 DoSomething(ref SomeStruct a, ref SomeStruct b)
{
return math.rotate(a.Rotation, a.Position) +
math.rotate(b.Rotation, b.Position);
}
public unsafe void Execute()
{
var InDataACopy = InDataA;
var InDataBCopy = InDataB;
OutData = DoSomething(ref InDataACopy, ref InDataBCopy);
}
}
所以编译器有:
- 将参数 a 和 b 更改为通过引用传递的“DoSomething”函数。
- 在 Execute 方法中添加了两个新的局部变量 InDataACopy 和 InDataBCopy。
- 它必须将 InDataA 和 InDataB 中的数据复制到这些变量中。
- 然后调用 DoSomething 函数,通过引用传递这些局部变量。
现在,如果我们再次查看 Burst Inspector 输出:
完