【简介】
Burst是Unity的编译优化技术,优化了从C#代码编译成Native代码的过程,经过编译优化后代码有更高的运行效率。
在Unity中使用Burst很简单,在方法或类前加上[BurstCompile]特性即可。在构建时编译代码的步骤,Burst编译器会识别该特性对方法或类做编译优化。其适用于高性能计算的场景,逻辑复杂的场景不适用。
Burst编译的实现得益于已有的SIMD和LLVM技术。
【SIMD】
在现代 CPU 中,并行性操作大致分为三种类型:
- 指令级并行,主要由 cpu 流水线技术,乱序执行技术等技术完成
- 线程级并行,主要依靠多核多线程技术实现
- 数据级并行,主要依靠 SIMD (单指令多数据) 来实现
SIMD是CPU硬件设计的一部分,是的CPU可以同时对多个数据执行相同的操作。
指令执行时(指令由操作码和操作数构成)CPU先访问缓存,缓存分为指令缓存和数据缓存,如果缓存中有指令就读取,没有指令从内存中读取;读取数据的过程也类似。
以加法为例,对于5+2这个加法计算,最少需要四条指令:
- 将数值 5 加载到寄存器中(LOAD操作码)
- 将数值 2 加载到寄存器中(LOAD操作码)
- 将两个寄存器中的数值相加(ADD操作码)
- 将相加后的结果存储到指定的寄存器或内存中(STORE操作码)
在SISD(单指令单数据)中,每个指令只会取一次数据,而在SIMD(单指令多数据)中,一次指令会取多个数据,节省了CPU获取数据的时间。
一次性取出来的数放在向量寄存器中,但向量寄存器大小有限,在AVX指令集中有256位。
在C#中,float类型占64位,一般要4个float类型一组。
现代编译器有三种方式来支持 SIMD:
- 编译器能够在没有用户干预的情况下生成支持使用硬件SIMD的代码,称之为自动矢量化
- 通过使用的Intrinsics 函数实现 SIMD
- 使用矢量 C++ 类 (仅限ICC编译器) 来实现 SIMD
(Intrinsics函数是一种内建函数,它们用于实现高级别的底层操作。这些函数通常由编译器提供,并且可以直接映射到特定的硬件指令。使用Intrinsics函数可以实现对特定硬件功能的直接访问,从而提高代码的性能和效率。
内联函数通常用关键字inline声明,是由编译器处理的普通函数,编译器会根据需要将函数内容直接插入到调用处,减少了函数调用的开销。而Intrinsics函数一般不需要显式声明,编译器会自动识别并将其优化为特定的硬件指令 )
Unity.Mathematics提供了支持SIMD的数据类型,例如float4/int4等类型,在Job中需要使用这些数据类型。
在Job中要多用float4类型才有效,例如有一个float类型的数组,可以转为float4类型的数组,有利于在循环计算时自动矢量化。
【LLVM】
传统编译器架构为:
frontEnd(前端)检查源代码是否存在错误,将源代码分析成词法单元,然后进行语法分析生成抽象语法树,生成中间IR语言
Optimizer(优化) 对中间IR语言进行优化,消除冗余计算,内联、变量折叠等
BackEnd(后端) 最终将IR中间语言生成目标机器所能执行的代码
这种问题在于不同语言都要有各自的编译器和优化手段,LLVM期望提供一种IR标准,不同语言经过编译后先生成LLVM IR语言,只需要针对其做优化即可。
新增一个语言时,只需新实现一个前端,新增一种设备后,只需新实现一个后端
BurstCompiler会根据特性找到需要编译的方法,将方法对应的C# IR转为LVVM IR,随后针对LVVM IR做优化,编译。
【Burst使用】
支持的类型和语法
可以简单的将BurstCompile特性至于方法或类上,但不是所有的方法和类都支持。情况如下:
- 支持基元类型:bool/byte/int/long/float/double,不支持char、string、decimal
- 支持Unity.Mathematics中的向量类型,例如bool3\bool4\int3\int4\float3\float4等
- 支持枚举类型、结构体、元组、System.IntPtr、Span<T>
- 支持DllImport and internal calls
- 不支持Managed Array,例如int[] a;支持NativeArray
- 方法中不能引用managed object
- 不支持Try Catch语法
打Log
代码中Log是少不了的,Burst对Debug.Log有额外支持,例如:
Debug.Log("This a string literal");
int value = 256;
Debug.Log($"This is an integer value {value}");
//string需要使用FixedString,例如:
FixedString128Bytes str = "fixedstring128";
Debug.Log(str);
编译检查
为避免在构建时才发现Burst编译不过,提供了Burst Inspector可以在Editor下预编译,以便查看编译结果
通过Jobs > Burst > Open Inspector 打开可视化面板,面板会显示编译结果
面板左边显示了可以正常编译得Job,右边显示当前选中的Job的编译结果,依次是:
- Assembly:最终优化生成的NativeCode
- .NET IL:原始的C# Job代码生成的IL
- LLVM IR (Unoptimized) :未优化的LLVM IR
- LLVM IR (Optimized):优化后的LLVM IR
- LLVM IR Optimization Diagnostics:提供优化诊断细节
舍弃编译
当在类或结构体添加BurstCompile特性后,默认对其内所有方法做编译,如果某个方法我们使用了Managed Object而不想编译,可以使用[BurstDiscard]特性。
注意,使用该特性时,方法不能有返回值,可以通过ref或者out传递返回值
同步编译
Editor上Job是默认异步编译的,首次执行到Job时才会异步编译。通过[BurstCompile(CompileSynchronously = true)]开启同步编译
精度设定
浮点数精度会影响计算性能,默认采用中等精度,可以通过[BurstCompile(FloatPrecision.Med, FloatMode.Fast)]指定精度
指定取值范围
对参数和返回值指定取值范围有利于编译器做特定的代码优化,例如
[return:AssumeRange(0u, 13u)]
static uint WithConstrainedRange([AssumeRange(0, 26)] int x)
{
return (uint)x / 2u;
}
分支指定
可以通过Unity.Burst.CompilerServices.Hint.Likely告诉编译器该分支大概率为true,需要重点优化,例如
if (Unity.Burst.CompilerServices.Hint.Likely(b))
{
// Any code in here will be optimized by Burst with the assumption that we'll probably get here!
}
else
{
// Whereas the code in here will be kept out of the way of the optimizer.
}
【参考】
深入浅出让你理解什么是LLVM - 简书
https://zhuanlan.zhihu.com/p/472813616
Quick Start | Burst | 1.7.4