目录
一:简介
1.1 Getting started
1.2 C# language support
1.2.1 HPC# overview
1.2.1.1 Exception expressions
1.2.1.2 Foreach and While
1.2.1.3 Unsupported C# features in HPC#
1.2.2 Static read-only fields and static constructor support
1.2.3 String support
1.2.4 Function Pointers
1.2.4.1 Using function pointers
1.2.4.2 Performance表现
1.2.5 C#/.NET type support
1.2.6 C#/.NET System namespace support
1.2.7 DllImport and internal calls
2.1 Burst intrinsics overview
2.1.3 简介
2.1.1 SIMD 和 SISD:
2.1.2 CPU单核和多核:
2.1.4 Burst Intrinsics Common class
3.1 Editor Reference
3.1.1.1 Show Timings setting
3.1.2 Burst Inspector window reference
3.2 Compilation
3.2.1 Synchronous compilation
3.2.2 BurstCompile attribute
3.2.3 BurstDiscard
3.2.4 Generic jobs
3.2.5 Compilation warnings
4.1 Building your project
5.1 Optimization
5.1.1 Debugging and profiling tools
5.1.1.1 Profiling Burst-compiled code
5.1.2 Loop vectorization
5.1.3 Memory aliasing
5.1.3.1 No Alias attribute
5.1.3.2 Aliasing and the job system
5.1.4 AssumeRange attribute
5.1.5 Hint intrinsics
一:简介
- 不使用Burst的编译流程:
- 不使用IL2CPP:
- c#->Roslyn编译器编译成IL字节码->运行时通过Mono虚拟机转换成目标平台的机器码
- 使用IL2CPP:
- c#->Roslyn 编译器编译成IL字节码->IL2CPP编译器转换成C++代码->特定平台的编译器,编译成特定平台的机器码(绕过了mono虚拟机,增加了安全性,快捷性,裁剪了无用代码)
- 不使用IL2CPP:
- Burst是一个编译器,封装了LLVM编译器,把IL字节码,编译成优化后的机器码,它专注于优化那些通过Unity的Job System和ECS编写的高性能代码片段
- IL2CPP和Burst编译器可以并行工作:IL2CPP负责将项目中的大部分C#代码(包括Unity脚本、第三方库等)转换成本地机器代码,以便在不同平台上运行。
"本地机器码"通常指的就是CPU可以直接执行的指令,也被称为"CPU字节码"或简单地说是"机器码"。这些指令是针对特定CPU架构设计的,比如x86, ARM等,它们由一系列的二进制代码组成,这些代码可以直接被CPU解读和执行。 不同的CPU架构有不同的指令集,即它们能理解和执行的机器码指令集合。因此,将程序编译成本地机器码意味着它被转换成了特定CPU架构能够直接执行的指令序列。这是为什么同一个高级语言编写的程序(如C#或C++)需要为不同的目标平台(如Windows上的x86或Android上的ARM)分别编译的原因。
1.1 Getting started
Burst 主要用来和Unity's job system一起使用,为Job struct 添加[BurstCompile]属性, 或者给类型,静态方法,添加[BurstCompile]属性,标记改方法或者改类型,使用Burst编译器。
// Using BurstCompile to compile a Job with Burst
[BurstCompile]
private struct MyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public void Execute()
{
float result = 0.0f;
for (int i = 0; i < Input.Length; i++)
{
result += Input[i];
}
Output[0] = result;
}
}
1.2 C# language support
Burst 使用了C#的一个高性能子集,叫作High Performance C# (HPC#) ,它与c#有很多限制和区别
1.2.1 HPC# overview
HPC#支持c#中的大多数表达式和语句。它支持以下功能:
Supported feature | Notes |
---|---|
Extension methods. | 支持扩展方法 |
Instance methods of structs. | 支持结构体的实例方法 |
Unsafe code and pointer manipulation. | unsafe的code和指针 |
Loading from static read-only fields. | Static read-only fields and static constructors. |
Regular C# control flows. | if else、 switch case、 for while break continue |
ref and out parameters | 支持ref、out |
fixed | 支持fixed关键字,表示在fixed块被执行完之前,不能被垃圾回收,内存被固定 |
Some IL opcodes. | cpblk、 initblk、 sizeof |
DLLImport and internal calls. | DLLImport and internal calls. |
| Burst如果发生异常,和c#表现不同,c#会执行的finally块,burst不会执行到finally块,而是会抛出来
|
Strings、ProfilerMarker . | Support for Unity Profiler markers. |
throw expressions. | Burst 只支持简单的throw模式, 比如:
|
Strings and Debug.Log . | String support and Debug.Log. |
Burst还提供了HPC#不能直接访问的C#方法的替代方案:
- Function pointers 替代委托
- Shared static 可以访问可以修改的静态字段
1.2.1.1 Exception expressions
Burst支持throw
exception.在editor下可以捕捉,在运行时,就会crash,所以要确保exception被捕捉到,通过给方法添加[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]捕捉异常,如果不添加,会有警告:
Burst warning BC1370: An exception was thrown from a function without the correct [Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")] guard. Exceptions only work in the editor and so should be protected by this guard
1.2.1.2 Foreach and While
Burst 对foreach、while 某种情况下不支持 - 采用一个或多个泛型集合参数的方法 T: IEnumerable<U>
不支持:
public static void IterateThroughConcreteCollection(NativeArray<int> list)
{
foreach (var element in list)
{
// This works
}
}
public static void IterateThroughGenericCollection<S>(S list) where S : struct, IEnumerable<int>
{
foreach (var element in list)
{
// This doesn't work
}
}
IterateThroughConcreteCollection()参数是一个确定的类型
NativeArray<int>
. IterateThroughGenericCollection()
参数是一个泛型参数,它的代码,不会被Burst编译器编译:
Can't call the method (method name) on the generic interface object type (object name). This may be because you are trying to do a foreach over a generic collection of type IEnumerable.
1.2.1.3 Unsupported C# features in HPC#
HPC# 不支持
try
/catch的catch
- 存储到静态字段,或者使用 Shared Static
- 任何关于托管对象的方法, for example, string methods.
1.2.2 Static read-only fields and static constructor support
不支持静态的非只读的数据,因为只读的静态数据,在编译时,就会替换了,如果编译失败,就会替换成默认值
1.2.3 String support
Burst支持下面几种string的用法:
- Debug.Log,支持字符串内插,内插的值必须是value type,除了half
Unity.Collection里的
FixedString 结构体,比如:FixedString128Bytes.- System.Runtime.CompilerServices 属性
[CallerLineNumber]
,[CallerMemberName]
,[CallerFilePath]
字符串不能传递给方法,或者作为struct的字段.可以使用 Unity.Collections库里面的
FixedString结构体:
int value = 256;
FixedString128 text = $"This is an integer value {value} used with FixedString128";
MyCustomLog(text);
// String can be passed as an argument to a method using a FixedString,
// but not using directly a managed `string`:
public static void MyCustomLog(in FixedString128 log)
{
Debug.Log(text);
}
1.2.4 Function Pointers
使用 FunctionPointer<T>替代c#的委托,因为delegates是托管对象,所以Burst不支持
Function pointers 不支持泛型委托. 也不要在另一个泛型方法中封装BurstCompiler.CompileFunctionPointer<T> ,否则,Burst不会生效,比如代码优化,安全检查
1.2.4.1 Using function pointers
在类上添加[BurstCompile]
属性在类的静态方法上添加[BurstCompile]
属性- 声明一个委托,标记这些静态方法
在绑定委托的方法上添加[MonoPInvokeCallbackAttribute]属性
. 这样IL2CPP才能正常使用
-
// Instruct Burst to look for static methods with [BurstCompile] attribute [BurstCompile] class EnclosingType { [BurstCompile] [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] public static float MultiplyFloat(float a, float b) => a * b; [BurstCompile] [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] public static float AddFloat(float a, float b) => a + b; // A common interface for both MultiplyFloat and AddFloat methods public delegate float Process2FloatsDelegate(float a, float b); }
- 然后在C#中声明这些委托:
FunctionPointer<Process2FloatsDelegate> mulFunctionPointer =
BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat);
FunctionPointer<Process2FloatsDelegate> addFunctionPointer =
BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(AddFloat);
- 在job中使用:
// Invoke the function pointers from HPC# jobs
var resultMul = mulFunctionPointer.Invoke(1.0f, 2.0f);
var resultAdd = addFunctionPointer.Invoke(1.0f, 2.0f);
Burst默认以异步方式编译function pointers,[BurstCompile(SynchronousCompilation = true)]强制同步编译
在C# 中使用function point,最好先缓存FunctionPointer<T>.Invoke
属性,它就是委托的一个实例:
private readonly static Process2FloatsDelegate mulFunctionPointerInvoke =
BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat).Invoke;
// Invoke the delegate from C#
var resultMul = mulFunctionPointerInvoke(1.0f, 2.0f);
1.2.4.2 Performance表现
最好在job里面使用function pointer,Burst为job提供了better aliasing calculations
不能给
function pointers直接传递[NativeContainer]
结构体,比如NativeArray,必须使用
job struct,Native container包含了用于安全检查safety check的托管对象。
下面是一个不好的例子:
///Bad function pointer example
[BurstCompile]
public class MyFunctionPointers
{
public unsafe delegate void MyFunctionPointerDelegate(float* input, float* output);
[BurstCompile]
public static unsafe void MyFunctionPointer(float* input, float* output)
{
*output = math.sqrt(*input);
}
}
[BurstCompile]
struct MyJob : IJobParallelFor
{
public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer;
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index)
{
var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr();
var outputPtr = (float*)Output.GetUnsafePtr();
FunctionPointer.Invoke(inputPtr + index, outputPtr + index);
}
}
不好的点在于:
- Burst不能矢量化function pointer ,因为它的参数是标量,这会损失4-8倍的性能
MyJob知道
Input
和Output
是native arrays不能alisa,但是function pointer不知道- There is a non-zero overhead to constantly branching to a function pointer somewhere else in memory.
[BurstCompile]
public class MyFunctionPointers
{
public unsafe delegate void MyFunctionPointerDelegate(int count, float* input, float* output);
[BurstCompile]
public static unsafe void MyFunctionPointer(int count, float* input, float* output)
{
for (int i = 0; i < count; i++)
{
output[i] = math.sqrt(input[i]);
}
}
}
[BurstCompile]
struct MyJob : IJobParallelForBatch
{
public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer;
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index, int count)
{
var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr() + index;
var outputPtr = (float*)Output.GetUnsafePtr() + index;
FunctionPointer.Invoke(count, inputPtr, outputPtr);
}
}
好的点在于:
- Burst 矢量化了
MyFunctionPointer方法
. - Burst 在每一个function pointer 处理
count个item,
调用函数指针的任何开销都减少了count次. - 批处理的性能比不批处理的性能提高1.53倍。
Burst使用IL Post Processing自动把代码,转成function pointer调用
但是最好还是:
[BurstCompile]
struct MyJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index)
{
Output[i] = math.sqrt(Input[i]);
}
}
addDisableDirectCall = true可以关闭自动转换
[BurstCompile]
public static class MyBurstUtilityClass
{
[BurstCompile(DisableDirectCall = true)]
public static void BurstCompiled_MultiplyAdd(in float4 mula, in float4 mulb, in float4 add, out float4 result)
{
result = mula * mulb + add;
}
}
1.2.5 C#/.NET type support
Burst使用 .NET的一个字集,不允许使用任何托管对象或者引用类型,比如class
- 内置类型
- 支持的:
bool
byte
/sbyte
double
float
int
/uint
long
/ulong
short
/ushort
- 不支持的:
char
decimal
string
:因为是托管类型
- 支持的:
- 数组类型:
- 支持的
- 静态只读的数组
- 不能作为方法的参数
- C#不使用job的代码,不能更改数组的数据,也就是只有job里面才可以更改,因为 Burst 编译器会在编译的时候copy一份数据.
- 不支持的
- 不支持多维数组
- 不支持托管数据,但可以使用NativeArray
- 支持的
- 结构体类型
- 支持的
- 含有上面类型的常规结构体
- 具有固定长度数组的结构体,就是数组一开始就声明长度
- 具有explicit layout的结构体可能不会生成最优代码
- 支持的layout:
LayoutKind.Sequential
LayoutKind.Explicit
StructLayoutAttribute.Pack
StructLayoutAttribute.Size
- 支持的layout:
- 支持含有
System.IntPtr、
System.UIntPtr字段,作为原生属性
- 支持的
Vector类型
- Burst会把 Unity.Mathematics 的vector类型转换成适合SIMD vector类型 :
bool2
/bool3
/bool4
uint2
/uint3
/uint4
int2
/int3
/int4
float2
/float3
/float4
- 优先使用
bool4
,uint4
,float4
,int4类型
- Burst会把 Unity.Mathematics 的vector类型转换成适合SIMD vector类型 :
- 枚举类型
- 支持
- 常规类型以及带有特定存储类型的类型,比如:public enum MyEnum : short
- 不支持
- 不支持枚举类型的方法,比如: Enum.HasFlag
- 不支持枚举类型的方法,比如: Enum.HasFlag
- 支持
- 指针类型
- 支持所有支持类型的指针
1.2.6 C#/.NET System namespace support
Burst 会把system命名空间下变量的转换成与Burst兼容的变量
System.Math
- 支持
System.Math下所有的方法
: double IEEERemainder(double x, double y)支持持
.NET Standard 2.1以上
- 支持
System.IntPtr
- 支持
System.IntPtr
/System.UIntPtr下的所有方法,包括静态字段
IntPtr.Zero、
IntPtr.Size
- 支持
System.Threading.Interlocked
- Burst支持
System.Threading.Interlocked下的所有方法,即线程安全
(比如Interlocked.Increment
).
- Burst支持
确保interlocked methods的source 位置是对齐的,比如:指针的对齐方式是指向类型的倍数:
[StructLayout(LayoutKind.Explicit)]
struct Foo
{
[FieldOffset(0)] public long a;
[FieldOffset(5)] public long b;
public long AtomicReadAndAdd()
{
return Interlocked.Read(ref a) + Interlocked.Read(ref b);
}
}
System.Threading.Thread
- 支持其中的
MemoryBarrier方法
- 支持其中的
System.Threading.Volatile
- Burst 支持非泛型的变量
Read、
Write
方法,该参数表示多个线程共享改变
- Burst 支持非泛型的变量
1.2.7 DllImport and internal calls
调用native plugin下面的方法,使用 [DllImport]:
[DllImport("MyNativeLibrary")]
public static extern int Foo(int arg);
Burst也支持Unity内部实现的的内部调用:
// In UnityEngine.Mathf
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern int ClosestPowerOfTwo(int value);
DllImport仅支持
native plug-ins下的方法,不支持独立于平台的dll,比如:kernel32.dll
.
DllImport支持的类型有
:
Type | Supported type |
---|---|
Built-in and intrinsic types | byte / sbyte short / ushort int / uint long / ulong float double System.IntPtr / System.UIntPtr Unity.Burst.Intrinsics.v64 / Unity.Burst.Intrinsics.v128 / Unity.Burst.Intrinsics.v256 |
Pointers and references | sometype* : 指针类型ref sometype : 引用类型 |
Handle structs | unsafe struct MyStruct { void* Ptr; } : 只包含一个指针类型的Struct unsafe struct MyStruct { int Value; } : 只包含一个整数类型的Struct |
需要通过指针或者引用传递structs,不能通过值类型传递,除了上面支持的类型,handle structs
1.2.8 SharedStatic struct
如果想在C#、HPC#共享静态的可变数据,使用 SharedStatic<T>
public abstract class MutableStaticTest
{
public static readonly SharedStatic<int> IntField = SharedStatic<int>.GetOrCreate<MutableStaticTest, IntFieldKey>();
// Define a Key type to identify IntField
private class IntFieldKey {}
}
C#、HPC# 都可以通过下面访问:
// Write to a shared static
MutableStaticTest.IntField.Data = 5;
// Read from a shared static
var value = 1 + MutableStaticTest.IntField.Data;
使用SharedStatic<T>时,需要注意
:
T
inSharedStatic<T>
定义了数据类型- 为了识别static 字段,提供一个上下文,为两个包含类型都创建一个键,比如:
MutableStaticTest、
IntFieldKey
- 在从hpc#访问共享静态字段之前,现在c#里面初始化它
2.1 Burst intrinsics overview
2.1.3 简介
Burst 提供了低阶的Api,在 Unity.Burst.Intrinsics 命名空间下,如果想写SIMD程序集代码,可以使用它下面的代码,获取额外的性能,就类似于底层代码。
2.1.1 SIMD 和 SISD:
- SISD单指令,单数据,一条指令如果执行多条数据,是串行的
- SIMD单指令,多数据,一条指令如果执行多条数据,是并行的
- 不同点:处理器不同,SIMD的处理器,能够处理多条数据
- 相同点:都会有指令集存储器,和寄存器来保存数据
2.1.2 CPU单核和多核:
- 单核:一个中央处理单元,一个核心,处理任务,只能一个人干,串行,单核提升速度的方法:提高时钟频率
- 多核:一个中央处理单元,多个核心,处理任务,多个人干,并行。比如:多个应用开启多个线程,就可以多个核心一起干
- 时钟频率:单位Hz,表示一秒执行多少次周期,比如3GHz,表示一秒进行30亿次,一般说Hz越高,运行速度越快,一条指令可能需要多个周期,单核的时候,单纯的提高时钟频率,会增加耗能、发热
- HZ的单位有:
- - KHz(千赫兹):1 kHz = 10^3 Hz(一千赫兹)
- - MHz(兆赫兹):1 MHz = 10^6 Hz(一百万赫兹)
- - GHz(吉赫兹):1 GHz = 10^9 Hz(十亿赫兹)
- - THz(太赫兹):1 THz = 10^12 Hz(一万亿赫兹)
- HZ的单位有:
2.1.4 Burst Intrinsics Common class
Unity.Burst.Intrinsics.Common 提供了在Burst支持的硬件上通用的功能。
- Unity.Burst.Intrinsics.Common.Pause :CPU 暂停当前线程,在x86上
pause,在ARM上yield,
在多线程编程中,尤其是在实现自旋锁(spinlock)或者在等待某个条件变为真时,直接使用忙等待循环(即不断地检查条件是否满足,而不进行休眠)会导致CPU在这段时间内高速运行,消耗大量的处理器资源。如果使用`Pause`指令,它会提示CPU在这种忙等待的场景下稍微“放慢脚步”,这样可以减少对CPU资源的消耗,同时对于等待的线程来说,延迟的增加是非常小的,几乎可以忽略不计- 自旋锁:
- 自旋锁(Spinlock)是一种用于多线程同步的锁机制,主要用于保护共享资源或临界区。与传统的锁(如互斥锁)不同,当一个线程尝试获取一个已经被其他线程持有的自旋锁时,它不会立即进入休眠状态(即阻塞状态),而是在锁被释放之前,持续在一个循环中检查锁的状态(这个过程称为“自旋”)。这意味着线程会一直占用CPU进行循环,直到它能够获取到锁。
- 自旋锁的优点:
- 当锁被占用的时间非常短时,它可以避免线程的上下文切换开销,因为线程不会进入休眠状态。这使得自旋锁在某些情况下比传统的锁更高效,尤其是在多核处理器上处理高并发且锁持有时间短的场景。
- 缺点:
- 1. **CPU资源消耗**:自旋锁在等待锁释放期间会持续占用CPU,如果锁被长时间持有,这将导致大量的CPU资源浪费。
- 2. **不适用于单核处理器**:在单核处理器上,自旋锁可能导致更差的性能,因为持有锁的线程可能无法释放锁,因为等待锁的线程持续占用CPU不让出执行机会。
- 3. **饥饿问题**:在某些情况下,自旋锁可能导致线程饥饿,即某些线程可能永远无法获取到锁,因为总有其他线程比它更早地获取到锁。
- 因此,自旋锁的使用需要仔细考虑其适用场景,通常是在多核处理器、锁持有时间非常短且对性能要求极高的情况下。在其他情况下,可能需要考虑使用其他类型的锁或同步机制
- 互斥锁:
- 当一个线程尝试获取一个已经被其他线程持有的锁时,该线程会进入阻塞状态。在这种情况下,操作系统会进行线程上下文切换,将CPU的控制权转移给另一个线程。这个过程涉及到保存当前线程的状态(例如寄存器、程序计数器等)并恢复另一个线程的状态,以便另一个线程可以继续执行。线程上下文切换是一个相对昂贵的操作,因为它涉及到一系列的硬件和操作系统层面的操作。
Unity.Burst.Intrinsics.Common.Prefetch
是一个实验性的内在特性,用于将特定的内存地址上的数据预加载到CPU缓存中,目的是为了在实际访问这些数据之前减少访问延迟,从而提高执行效率。使用prefetch可以在进行大量内存读取操作的循环中提高性能,尤其是对于那些访问模式可预测的循环操作。然而,正确地使用prefetch需要对所处理数据的内存访问模式有很好的理解,错误的使用可能不会带来任何性能上的改善,甚至可能使性能更差。因为是实验行的,所以要UNITY_BURST_EXPERIMENTAL_PREFETCH_INTRINSIC来访问
- Unity.Burst.Intrinsics.Common.umul128:用于执行无符号的128位乘法操作。这个函数接受两个64位无符号整数作为输入,执行它们的乘法,并返回128位的乘积结果。由于直接在C#中进行128位整数运算并不直接支持,这个函数提供了一种在需要进行大数乘法时的有效手段,尤其是在性能敏感的应用场景中。
- 具体来说,`umul128`函数会返回一个包含两个64位无符号整数的元组或结构体,这两个整数分别代表乘积的低64位和高64位。这样,开发者可以在不丢失精度的情况下处理大于64位的乘法运算结果。
- 使用`umul128`可以在进行大整数运算、加密算法、随机数生成等需要高精度和大范围数值计算的场景中非常有用。然而,由于它是一个低级的内联函数,使用时需要对数字运算有一定的理解,以确保正确处理乘法的结果。
Unity.Burst.Intrinsics.Common.InterlockedAnd
和Unity.Burst.Intrinsics.Common.InterlockedOr 提供了int
,uint
,long
,ulong类型的原子属性上的且或操作,因为是实验行的,使用宏
UNITY_BURST_EXPERIMENTAL_ATOMIC_INTRINSICS声明访问
3.1 Editor Reference
3.1.1 Burst menu reference
Enable Compilation | Burst编译带有 [BurstCompile]的 jobs 和自定义 delegates |
Enable Safety Checks | Enable Safety Checks setting |
Off | 关闭安全检查在jobs和function-pointers上,可以获取额外的真实的性能 |
On | 对collection containers (e.g NativeArray<T> )开启安全检查,包括job data依赖和是否越界 |
Force On | 即使 DisableSafetyChecks = true也安全检查 |
Synchronous Compilation | Synchronous compilation. |
Native Debug Mode Compilation | 关闭burst对代码的优化 Native Debugging tools. |
Show Timings | 显示burst编译的时间 Show Timings setting |
Open Inspector | Opens the Burst Inspector window. |
3.1.1.1 Show Timings setting
开启Show Timings时,Unity会打印出来,Burst编译每个库的入口点时间,Burst会把一个程序集的所有方法,编译成一个单元,批处理,把多个entry-points成组,组成一个task.
Burst的工作主要分为下面几步:
- 找出需要burst编译的所有方法
- front end找到之后,Burst将c# IL转换为LLVM IR模块
- middle end然后Burst specializes, optimizes, cleans up
- back end最后Burst把LLVM IR module转换成native DLL)
front end编译的时间,和需要编译的方法,成正比关系,泛型越多,时间越长,因为每个类型都编译一遍
back-end 的时间与entry-point的数量成正比,以为每一个entry point都是一个单独的文件。比如一个脚本。
如果optimize花费了大量的时间,通过[BurstCompile(OptimizeFor = OptimizeFor.FastCompilation)]可以减少优化的的内容,同时也会变快。
3.1.2
Burst Inspector window reference
Burst Inspector窗口展示了所有Burst编译的jobs和其它对象,Jobs > Burst > Open Inspector.
3.2 Compilation
- 在Play mode,Burst通过just-in-time (JIT)编译,异步编译,表示在Burst编译完之前,都是使用Mono编译器编译的代码,如果不想异步编译,请看Synchronous compilation
- 在发布的时候,Burst通过ahead-of-time (AOT)编译,通过Playersetting窗口,控制编译的方式,详情:Building your project
3.2.1 Synchronous compilation
默认,Burst异步编译jobs,在play mode 模式下,通过 CompileSynchronously = true,表示同步编译。
[BurstCompile(CompileSynchronously = true)]
public struct MyJob : IJob
{
// ...
}
如果不设置的话,当第一次运行这个job时,Burst 在后台线程中异步编译,与此同时,运行的是托管的c#代码。
当CompileSynchronously = true,它会影响当前帧,体验不好
,一般在下面情况中使用:
- 当想测试Burst编译后的代码时,同时忽略首次调用的时间
- 调试托管和编译代码之间的差异。
3.2.2 BurstCompile attribute
- 对数学方法使用不同的精度,比如sin,cos.
- 放松对数学计算的限制,这样burst就可以重新安排浮点数的计算顺序
- 强迫synchronous compilation of a job
[BurstCompile(FloatPrecision.Med, FloatMode.Fast)]
- FloatPrecision:单位ulp,表示浮点数之间的空间
FloatPrecision.Standard
: 和FloatPrecision.Medium一样,精度
3.5 ulp.FloatPrecision.High
: 1.0 ulp.FloatPrecision.Medium
: 3.5 ulp.FloatPrecision.Low
: 每个函数都定义了精度,函数可以指定有限范围的有效输入。
- FloatMode
FloatMode.Default
: 和FloatMode.Strict一样
.FloatMode.Strict
: 不执行重排计算的顺序FloatMode.Fast
: 可以重排顺序,对于不需要精确的计算顺序可以使用FloatMode.Deterministic
: 为后面版本预留的
- ulp
- Unit in the Last Place,表示相邻两个浮点数之间的距离,它的大小取决于浮点数的精度和大小。 举个例子,假设我们有两个浮点数A和B,A < B,那么A和B之间的“空间”可以用它们之间相差的ULP数来描述。如果A和B之间正好相差1个ULP,那么意味着没有其他浮点数能够位于A和B之间;如果它们之间相差多个ULP,那么就存在其他浮点数可以位于A和B之间,指的是两个数值之间的差距,例如,比较两个浮点数是否接近可能需要计算它们之间的ULP差异,而不是直接比较它们的值。
给整个程序集添加burst编译属性
[assembly: BurstCompile(CompileSynchronously = true)]
3.2.3 BurstDiscard
添加上此属性,代码不会被Burst编译,也就是在job标价burst时,标记该属性的方法不会执行,添加该属性的方法,不能有返回值,可以通过ref、out来获取更改后的值
[BurstCompile]
public struct MyJob : IJob
{
public void Execute()
{
// Only executed when running from a full .NET runtime
// this method call will be discard when compiling this job with
// [BurstCompile] attribute
MethodToDiscard();
}
[BurstDiscard]
private static void MethodToDiscard(int arg)
{
Debug.Log($"This is a test: {arg}");
}
}
[BurstDiscard]
private static void SetIfManaged(ref bool b) => b = false;
private static bool IsBurst()
{
var b = true;
SetIfManaged(ref b);
return b;
}
3.2.4 Generic jobs
不支持嵌套job,如果使用了嵌套job,在editor下,burst能检测到,并使用burst编译,但是在build时,burst不会对这部分代码编译,所以editor下和运行时,两者的性能有差距。
比如:
直接使用泛型job
[BurstCompile]
struct MyGenericJob<TData> : IJob where TData : struct {
public void Execute() { ... }
}
或者包装一层,job不是泛型的,但可以使用Tdata数据
public class MyGenericSystem<TData> where TData : struct {
[BurstCompile]
struct MyGenericJob : IJob {
public void Execute() { ... }
}
public void Run()
{
var myJob = new MyGenericJob(); // implicitly MyGenericSystem<TData>.MyGenericJob
myJob.Schedule();
}
}
---------------------------------
嵌套类型,外部是泛型,内部也是泛型
public static void GenericJobSchedule<TData>() where TData: struct {
// Generic argument: Generic Parameter TData
// This Job won't be detected by the Burst Compiler at standalone-player build time.
var job = new MyGenericJob<TData>();
job.Schedule();
}
只能在editor下使用burst编译
GenericJobSchedule<int>();
3.2.5 Compilation warnings
使用 Unity.Burst.CompilerServices.IgnoreWarningAttribute可以忽略警告
- BC1370
- An exception was thrown from a function without the correct
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]
guard... - 如果使用了throw,但是没有catch,就会报这个,因为throw在运行时,会崩溃,加上
[Conditional("ENABLE_UNITY_COLLECTIONS_CHECKS")]属性,throw的方法在build时就会丢弃
- An exception was thrown from a function without the correct
- BC1371
- 当一个方法,被discard时,会报
4.1 Building your project
在build时,burst会编译代码,然后把它编译成一个动态链接库(dll),放在plugin文件夹下面,不同的平台,放的位置不一样,比如window:Data/Plugins/lib_burst_generated.dll
iOS例外,它生成的是一个静态库
job在运行时compile代码时,会首先加载dll,通过Edit > Player Settings > Burst AOT Settings设置Burst编译时的设置
5.1 Optimization
5.1.1 Debugging and profiling tools
- Editor:
- 可以使用rider、vs自带的debug工具,attach之后,unity会关闭burst优化,现在debug的就是托管代码。
- 也可以使用native debug工具,比如vs、xcode,同样会关闭burst优化,有两种方式:
- 一种是:开启Jobs > Burst > Native Debug Mode Compilation,它会关闭所有的burst优化
- 一种是:[BurstCompile(Debug = true)]只对某个job,关闭burst优化
- Player Mode
- 需要给debug tool指定burst生成的符号文件,一般是在plugin文件夹,在这之前需要开启生成符号文件的选项,有两种方式:
- Development Build
- Burst AOT Player Settings开启Force Debug Information
- 同时需要关闭 Burst optimizations,有两种方式:
Debug = true,关闭指定的job
- 关闭Burst AOT Player Settings,Enable Optimizations选项,是关闭所有job
- 需要给debug tool指定burst生成的符号文件,一般是在plugin文件夹,在这之前需要开启生成符号文件的选项,有两种方式:
- System.Diagnostics.Debugger.Break System.Diagnostics.Debugger.Break 方法,可以在debuger attach的时候,触发,其它的时候不触发,就相当于断点
5.1.1.1 Profiling Burst-compiled code
想要分析burst编译后的代码,可以在 Instruments 或者 Superluminal里,分析编译后的代码,前提先指定,burst编译后的符号文件
可以通过playermarker,对debug的代码做标记:
[BurstCompile]
private static class ProfilerMarkerWrapper
{
private static readonly ProfilerMarker StaticMarker = new ProfilerMarker("TestStaticBurst");
[BurstCompile(CompileSynchronously = true)]
public static int CreateAndUseProfilerMarker(int start)
{
using (StaticMarker.Auto())
{
var p = new ProfilerMarker("TestBurst");
p.Begin();
var result = 0;
for (var i = start; i < start + 100000; i++)
{
result += i;
}
p.End();
return result;
}
}
}
5.1.2 Loop vectorization
简单来讲就是把确定了的运算,进行SMID写法处理
[MethodImpl(MethodImplOptions.NoInlining)]
private static unsafe void Bar([NoAlias] int* a, [NoAlias] int* b, int count)
{
for (var i = 0; i < count; i++)
{
a[i] += b[i];
}
}
public static unsafe void Foo(int count)
{
var a = stackalloc int[count];
var b = stackalloc int[count];
Bar(a, b, count);
}
生成的汇编语言
.LBB1_4:
vmovdqu ymm0, ymmword ptr [rdx + 4*rax]
vmovdqu ymm1, ymmword ptr [rdx + 4*rax + 32]
vmovdqu ymm2, ymmword ptr [rdx + 4*rax + 64]
vmovdqu ymm3, ymmword ptr [rdx + 4*rax + 96]
vpaddd ymm0, ymm0, ymmword ptr [rcx + 4*rax]
vpaddd ymm1, ymm1, ymmword ptr [rcx + 4*rax + 32]
vpaddd ymm2, ymm2, ymmword ptr [rcx + 4*rax + 64]
vpaddd ymm3, ymm3, ymmword ptr [rcx + 4*rax + 96]
vmovdqu ymmword ptr [rcx + 4*rax], ymm0
vmovdqu ymmword ptr [rcx + 4*rax + 32], ymm1
vmovdqu ymmword ptr [rcx + 4*rax + 64], ymm2
vmovdqu ymmword ptr [rcx + 4*rax + 96], ymm3
add rax, 32
cmp r8, rax
jne .LBB1_4
汇编语言的意思:
这段代码是使用 x86-64 汇编语言(使用 AVX2 指令集)写的,它对应于提供的 C# 代码,实现了一个简单的向量加法操作。下面是一步一步地解释它的功能:
- 1. **函数 `Bar` 的循环展开与向量化**: 鉴于源代码的 `Bar` 函数负责将两个整数数组(或指针所指向的内存区域)的元素逐个相加,这段汇编代码采用了向量化的方式来提升性能。它一次处理128个字节(即32个`int`类型的数据,每个`int`占用了4个字节),这是因为使用的 `ymm` 寄存器可以一次性处理256位数据。
- 2. `vmovdqu ymm0, ymmword ptr [rdx + 4*rax]` 等四条 `vmovdqu` 指令用于从内存中加载数据到 `ymm0`、`ymm1`、`ymm2` 和 `ymm3` 寄存器中。在这里 `[rdx + 4*rax]` 和类似的表达式利用了 `rdx` 作为基址(表示数组 `b` 的起始地址),`rax` 作为索引值(起始值为 0,后续以32为步长递增,代替了循环变量 `i`,因为每次迭代处理32个 `int`),乘以4的原因是每个 `int` 占4字节,用于计算在 `b` 数组中正确的偏移量。
- 3. `vpaddd ymm0, ymm0, ymmword ptr [rcx + 4*rax]` 等四条 `vpaddd` 指令执行向量加法(`ymm` 寄存器与内存数据),将 `a` 数组中相应的值(由 `[rcx + 4*rax]` 等地址指定)与 `b` 数组的值相加,结果存回到相应的 `ymm` 寄存器里。
- 4. 接下来的四条 `vmovdqu` 指令将加法操作的结果 (`ymm0`, `ymm1`, `ymm2`, `ymm3`) 存回到 `a` 数组相应的位置。
- 5. `add rax, 32` 用于更新循环索引 `i`,跳到下一批次处理的起点,因为每次迭代处理了 32 个 `int`,所以 `rax` 每次增加 32。
- 6. `cmp r8, rax` 与 `jne .LBB1_4` 这一条件跳转指令组合实现了循环的继续判断。如果 `rax`(代表当前已处理的 `int` 的数量)还没有达到总数 `r8`(`count` 参数的值),汇编执行跳回标签 `.LBB1_4` 开始处理下一批数据。
综上所述,这段汇编代码通过一系列向量化指令并行地完成了数组 `a` 和 `b` 元素的加法操作,显著提高了原始 C# 代码循环中单个元素加法操作的执行效率。
5.1.3 Memory aliasing
它是一种告诉Burst代码中是如何使用数据的。这可以改善和优化应用程序的性能。
当内存中的位置相互重叠(overlap)时,就会发生Memory aliasing。下面概述了Memory aliasing和无Memory aliasing之间的区别。
[BurstCompile]
private struct CopyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public void Execute()
{
for (int i = 0; i < Input.Length; i++)
{
Output[i] = Input[i];
}
}
}
- No memory aliasing
如果Input
和Output
没有发生内存重叠, 就是它们的内存相互独立,就像下面的表示,如果是No Aliasing,Burst就会通过向量化,把已知的的标量给分成批次处理,比如一次处理32个int,而不是单独处理
Memory with no aliasing
Memory with no aliasing vectorized
- Memory aliasing
如果Output数组和
Input数组,有元素重叠,比如
Output[0]指向
Input[1],这就表示内存混叠,比如
:
Memory with aliasin
如果没有声明aliasing,它会自动矢量化,然后结果如下图所示,这样就会有bug,因为内存错位了,数值都变了:
Memory with aliasing and invalid vectorized code
- Generated code
CopyJob,针对x64指令集的子集AVX2生成的
汇编语言. 指令vmovups移动8个浮点数,所以一个自动向量化循环移动4 × 8个浮点数,这等于每次循环迭代复制32个浮点数,而不是一个:
.LBB0_4:
vmovups ymm0, ymmword ptr [rcx - 96]
vmovups ymm1, ymmword ptr [rcx - 64]
vmovups ymm2, ymmword ptr [rcx - 32]
vmovups ymm3, ymmword ptr [rcx]
vmovups ymmword ptr [rdx - 96], ymm0
vmovups ymmword ptr [rdx - 64], ymm1
vmovups ymmword ptr [rdx - 32], ymm2
vmovups ymmword ptr [rdx], ymm3
sub rdx, -128
sub rcx, -128
add rsi, -32
jne .LBB0_4
test r10d, r10d
je .LBB0_8
同样的代码,但是手动关闭了Burst aliasing,要比原来的性能低:
.LBB0_2:
mov r8, qword ptr [rcx]
mov rdx, qword ptr [rcx + 16]
cdqe
mov edx, dword ptr [rdx + 4*rax]
mov dword ptr [r8 + 4*rax], edx
inc eax
cmp eax, dword ptr [rcx + 8]
jl .LBB0_2
- Function cloning
- 对于不知道参数需不需要aliasing的方法,Burst通过copy一份方法副本,然后假设不生成alisa,来生成汇编代码,如果不报错,就替换原来的汇编代码(没有优化的代码)
[MethodImpl(MethodImplOptions.NoInlining)]
int Bar(ref int a, ref int b)
{
a = 42;
b = 13;
return a;
}
int Foo()
{
var a = 53;
var b = -2;
return Bar(ref a, ref b);
}
- 因为Burst不知道Bar方法里的a和b是否aliasing.,所以生成的汇编语言和其它编译器是一致的:
//dword ptr [rcx]先从rcx(寄存器)取值,然后把42赋值给它,mov
mov dword ptr [rcx], 42
mov dword ptr [rdx], 13
//取值rcx,把它赋值给eax
mov eax, dword ptr [rcx]
//表示控制权结束,return
ret
Burst比这更聪明,通过function cloning,Burst创建了Bar的副本,它推断这个副本中的属性不会发生混叠,生成不发生混叠时的代码,替换原来的调用,这样就不用从寄存器里面取数了:
mov dword ptr [rcx], 42
mov dword ptr [rdx], 13
mov eax, 42
ret
- Aliasing checks
- 因为aliasing是Burst进行优化的关键,所以提供了一些内部的检测方法:
- Unity.Burst.CompilerServices.Aliasing.ExpectAliased 表示两个指针会alias,如果没有则生成编译器错误。
- Unity.Burst.CompilerServices.Aliasing.ExpectNotAliased表示两个指针不会alias, 如果不是,则生成编译器错误。
- 因为aliasing是Burst进行优化的关键,所以提供了一些内部的检测方法:
比如:
using static Unity.Burst.CompilerServices.Aliasing;
[BurstCompile]
private struct CopyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public unsafe void Execute()
{
// NativeContainer attributed structs (like NativeArray) cannot alias with each other in a job struct!
ExpectNotAliased(Input.getUnsafePtr(), Output.getUnsafePtr());
// NativeContainer structs cannot appear in other NativeContainer structs.
ExpectNotAliased(in Input, in Output);
ExpectNotAliased(in Input, Input.getUnsafePtr());
ExpectNotAliased(in Input, Output.getUnsafePtr());
ExpectNotAliased(in Output, Input.getUnsafePtr());
ExpectNotAliased(in Output, Output.getUnsafePtr());
// But things definitely alias with themselves!
ExpectAliased(in Input, in Input);
ExpectAliased(Input.getUnsafePtr(), Input.getUnsafePtr());
ExpectAliased(in Output, in Output);
ExpectAliased(Output.getUnsafePtr(), Output.getUnsafePtr());
}
}
5.1.3.1 No Alias attribute
不需要alias,对于native container、job struct,一般不需要添加该属性,因为burst会主动推断是否需要Alisa。
只有那些Burst推断不出来的,可以添加该属性,前提是,明确知道标记的参数,不会进行Alisa,如果标记No Alisa的,实际情况需要Alisa,有可能产生bug
- 添加Alisa的情况:
- 方法的参数
- 方法的返回值
- 不会Alisa的结构体
- 不会Alisa的结构体的字段
比如:
int Foo([NoAlias] ref int a, ref int b)
{
b = 13;
a = 42;
return b;
}
----------------------------------------
struct Bar
{
[NoAlias]
public NativeArray<int> a;
[NoAlias]
public NativeArray<float> b;
}
int Foo(ref Bar bar)
{
bar.b[0] = 42.0f;
bar.a[0] = 13;
return (int)bar.b[0];
}
----------------------------------------
[NoAlias]
unsafe struct Bar
{
public int i;
public void* p;
}
float Foo(ref Bar bar)
{
*(int*)bar.p = 42;
return ((float*)bar.p)[bar.i];
}
----------------------------------------
[MethodImpl(MethodImplOptions.NoInlining)]
[return: NoAlias]
unsafe int* BumpAlloc(int* alloca)
{
int location = alloca[0]++;
return alloca + location;
}
unsafe int Func()
{
int* alloca = stackalloc int[128];
// Store our size at the start of the alloca.
alloca[0] = 1;
int* ptr1 = BumpAlloc(alloca);
int* ptr2 = BumpAlloc(alloca);
*ptr1 = 42;
*ptr2 = 13;
return *ptr1;
}
5.1.3.2 Aliasing and the job system
Unity's job system对job中的参数alias有一些限制:
- [NativeContainer] (比如 NativeArray and NativeSlice) 不能alisa
- Job struct中的字段标有 [NativeDisableContainerSafetyRestriction] 属性的,可以和其它字段Alisa.
[NativeContainer]
不能作为其它[NativeContainer]的子项
. 比如:NativeArray<NativeSlice<T>>
.
5.1.4 AssumeRange attribute
AssumeRange 属性,表示告诉burst标量的范围,如果burst知道该范围,会进行相关的优化,比如:
[return:AssumeRange(0u, 13u)]
static uint WithConstrainedRange([AssumeRange(0, 26)] int x)
{
return (uint)x / 2u;
}
有两个限制:
- 只可以添加到 (signed or unsigned) 整形上面.
- range的参数类型,必须和添加属性的类型一致.
Burst 已经对 NativeArray、
NativeSlice
的.Length属性做了替换,因为它永远是正的,比如
:
static bool IsLengthNegative(NativeArray<float> na)
{
// Burst 总是用常量false替换它
return na.Length < 0;
}
比如:下面表示_length是永远>0的
struct MyContainer
{
private int _length;
[return: AssumeRange(0, int.MaxValue)]
private int LengthGetter()
{
return _length;
}
public int Length
{
get => LengthGetter();
set => _length = value;
}
// Some other data...
}
5.1.5 Hint intrinsics
它告诉Burst优先优化分支内的代码,减少编译时间:
- Unity.Burst.CompilerServices.Hint.Likely: 很大可能为真
- Unity.Burst.CompilerServices.Hint.Unlikely: 很大可能为假
- Unity.Burst.CompilerServices.Hint.Assume: 为真,谨慎使用,相当于宏定义
判断的值还是b,只不过告诉burst,b很大可能为true
if (Unity.Burst.CompilerServices.Hint.Likely(b))
{
// 这里的任何代码都将被Burst优化
}
else
{
// 这里的代码不会被优化
}