DOTS:Burst

目录

一:简介

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

1.2.8 SharedStatic struct

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 Burst menu 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虚拟机,增加了安全性,快捷性,裁剪了无用代码)
  • 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 featureNotes
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 outparameters支持ref、out
fixed支持fixed关键字,表示在fixed块被执行完之前,不能被垃圾回收,内存被固定
Some IL opcodes.cpblk、initblk、sizeof
DLLImport and internal calls. DLLImport and internal calls.

try、finally关键字、IDisposable

using、foreach

Burst如果发生异常,和c#表现不同,c#会执行的finally块,burst不会执行到finally块,而是会抛出来

foreach支持特定的示例

Strings、ProfilerMarker.Support for Unity Profiler markers.
throwexpressions.

Burst 只支持简单的throw模式, 比如:

throw new ArgumentException("Invalid argument").  Burst 会提取异常的静态字符串消息并将其包含在生成的代码中。

Strings and Debug.Log.String support and Debug.Log.

Burst还提供了HPC#不能直接访问的C#方法的替代方案:

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的用法: 

字符串不能传递给方法,或者作为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才能正常使用
  1. // 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知道InputOutput是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
      • 支持含有System.IntPtr、System.UIntPtr字段,作为原生属性
  • Vector类型
    • Burst会把  Unity.Mathematics 的vector类型转换成适合SIMD vector类型 :
      • bool2/bool3/bool4
      • uint2/uint3/uint4
      • int2/int3/int4
      • float2/float3/float4
      • 优先使用bool4uint4float4int4类型
  • 枚举类型
    • 支持
      • 常规类型以及带有特定存储类型的类型,比如:public enum MyEnum : short
    • 不支持
  • 指针类型
    • 支持所有支持类型的指针

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).

确保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方法,该参数表示多个线程共享改变

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支持的类型有:

TypeSupported type
Built-in and intrinsic typesbyte / 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 referencessometype* : 指针类型
ref sometype : 引用类型
Handle structsunsafe 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 in SharedStatic<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(一万亿赫兹)

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 ChecksEnable Safety Checks setting 
Off关闭安全检查在jobs和function-pointers上,可以获取额外的真实的性能
On对collection containers (e.g NativeArray<T>)开启安全检查,包括job data依赖和是否越界
Force On即使 DisableSafetyChecks = true也安全检查
Synchronous CompilationSynchronous compilation.
Native Debug Mode Compilation关闭burst对代码的优化 Native Debugging tools.
Show Timings显示burst编译的时间 Show Timings setting 
Open InspectorOpens 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

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时就会丢弃
  • 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,有两种方式:​​​​​​​
  • 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 aliasing


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

比如:

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有一些限制:

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优先优化分支内的代码,减少编译时间:

判断的值还是b,只不过告诉burst,b很大可能为true
if (Unity.Burst.CompilerServices.Hint.Likely(b))
{
    // 这里的任何代码都将被Burst优化
}
else
{
    // 这里的代码不会被优化
}


 

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TO_ZRG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值