Burst用户指南
原文:https://docs.unity3d.com/Packages/com.unity.burst@0.2/manual/index.html#memory-aliasing-and-noalias
转自 : https://blog.csdn.net/alph258/article/details/83997917
Unity Burst User Guide
翻译不易,转载请注明原译者。
概观
Burst是一个编译器,它使用LLVM将IL / .NET字节码转换为高度优化的本机代码。它作为Unity包发布,并使用Unity Package Manager集成到Unity中。
快速开始
使用burst编译器编译Job
Burst主要用于与Job系统高效协作。
您可以通过使用属性[BurstCompile]装饰Job结构,从而在代码中简单地使用burst编译器 。
using Unity.Burst;using Unity.Collections;using Unity.Jobs;using UnityEngine;public class MyBurst2Behavior : MonoBehaviour
{
void Start()
{
var input = new NativeArray<float>(10, Allocator.Persistent);
var output = new NativeArray<float>(1, Allocator.Persistent);
for (int i = 0; i < input.Length; i++)
input[i] = 1.0f * i;
var job = new MyJob
{
Input = input,
Output = output
};
job.Schedule().Complete();
Debug.Log("The result of the sum is: " + output[0]);
input.Dispose();
output.Dispose();
}
// Using BurstCompile to compile a Job with burst
// Set CompileSynchronously to true to make sure that the method will not be compiled asynchronously
// but on the first schedule
[BurstCompile(CompileSynchronously = true)]
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;
}
}
}
默认情况下,在编辑器中,Burst JIT是通过异步来编译job,但在上面的示例中,我们使用该选项CompileSynchronously = true确保在第一个Schedule中编译该方法。通常,您应该使用异步编译。见[BurstCompile]选项
Jobs/Burst Menu菜单
Burst在Jobs菜单中添加了一些菜单项:
- 使用Burst Jobs:选中此项后,具有该属性[BurstCompile]的Jobs将被Burst编译。默认是勾选的。
- Burst Inspector:打开Burst Inspector窗口
- 启用Burst 安全检查:选中此选项后,将使用收集容器(例如NativeArray)的代码将检查安全用法,尤其是Jobs数据依赖性检查系统和容器索引超出界限。请注意,此选项默认情况下禁用noaliasing性能优化。默认是勾选的。
- 启用Burst 编译:选中此选项后,Burst 将编译Jobs和使用该属性[BurstCompile]标记的自定义委托。默认是勾选的。
-
显示Burst 耗时:当选中此项时,每次Burst
都必须在编辑器中JIT编译一个Jobs,编译此方法所需的时间将显示在日志中。默认为未勾选的。
Burst 属性面板
从“Jobs”菜单中,您可以打开Burst 属性面板。属性面板允许您查看可以编译的所有作业,然后您还可以检查生成的本机代码。
在这里插入图片描述
在左侧窗格中,我们有Compile Targets,它提供了一个可以编译的Jobs列表。以白色突出显示的作业可以通过Burst 编译,而禁用的作业则不具有该[BurstCompile]属性。
1.从左窗格中选择一个活动的编译目标。
2.在右窗格中,按“ 刷新反汇编 ”按钮
3.在不同选项卡之间切换以显示详细信息:
- 选项卡程序集(Assembly )提供了由burst生成的最终优化本机代码
- 选项卡.NET IL提供了从Job方法中提取的原始.NET IL的视图
- 选项卡LLVM(未优化)在优化之前提供内部LLVM IR的视图。
- 选项卡LLVM(优化)在优化后提供内部LLVM IR的视图。
- 选项卡LLVM IR Optimization Diagnostics提供优化的详细LLVM诊断(即,如果它们成功或失败)。
4.您还可以切换不同的选项:
- 如果启用“Safety Checks”将生成包括容器访问安全检查(如检查是否有作业写入本地容器是只读)的代码
- 如果启用“Optimizations ”此选项将允许编译器优化代码。
- 如果启用了“ Fast Math”选项,则编译器可以折叠数学运算以提高效率,但代价是不考虑精确的数学正确性(请参阅编译器放宽选项)
C#/ .NET语言支持
Burst正在研究.NET的一个子集,它不允许在代码中使用任何托管对象/引用类型(C#中的类)。
以下部分提供了更多有关burst实际支持的构造类型详细信息。
支持的.NET类型
原始类型
Burst支持以下原始类型:
- bool
- char
- sbyte/byte
- short/ushort
- int/uint
- long/ulong
- float
- double
Burst不支持以下类型: - string 因为这是一种托管类型
- decimal
矢量类型
Burst能够将矢量类型从Unity.Mathematics原生SIMD矢量类型转换为优化的第一类支持:
- bool2/bool3/bool4
- uint2/uint3/uint4
- int2/int3/int4
- float2/float3/float4
请注意,出于性能原因,应首选4种wide 类型(float4,int4…)
枚举类型
Burst支持所有枚举,包括具有特定存储类型的枚举(例如public enum MyEnum : short)
Burst目前不支持Enum方法(例如Enum.HasFlag)
结构类型
Burst支持具有支持类型的任何字段的常规结构。
Burst支持固定数组字段。
关于布局,LayoutKind.Sequential和LayoutKind.Explicit都受到支持,该StructLayout.Pack包装尺寸不支持
本机支持System.IntPtr和UIntPtr作为直接表示指针的内部结构。
指针类型
Burst支持任何Burst支持类型的指针类型
通用类型
Burst支持结构使用的泛型类型。具体来说,它支持对具有接口约束的泛型类型的泛型调用的完全实例化(例如,当具有通用参数的结构需要实现接口时)
数组类型
Burst不支持托管阵列。例如,您应该使用本机容器NativeArray。
语言支持
Burst支持以下代码流和语法:
- 常规C#控制流程:if/else/switch/case/for/while/break/continue
- 扩展方法
- 不安全的代码,指针操作…等等。
- 结构的实例方法
- 通过ref / out参数
- 调用icall / internal函数
- throw假设简单的抛出模式(例如throw new ArgumentException(“Invalid
argument”)),对表达式的支持有限。在这种情况下,我们将尝试提取静态字符串异常消息以将其包含在生成的代码中。 - 一些特殊的操作码IL像cpblk,initblk,sizeof
- 从静态只读字段加载
Burst不支持: - DllImport或calli(这应该在将来的版本中支持)
- catch
- try/ finally(将来某个时候)
- foreach因为它需要try/ finally(这应该在未来的版本中支持)
- 从非只读静态字段加载或存储到静态字段
- 任何与托管对象相关的方法(例如数组访问等)
内部函数
System.Math
Burst为声明的所有方法提供内在函数,System.Math但不支持以下方法:
- double IEEERemainder(double x, double y)
- Round(double value, int digits)
System.IntPtr
Burst支持System.IntPtr/的所有方法System.UIntPtr,包括静态字段IntPtr.Zero和IntPtr.Size
System.Threading.Interlocked
Burst支持由System.Threading.Interlocked(例如Interlocked.Increment…等)提供的所有方法的原子内存内在函数
NativeArray
Burst 仅支持以下NativeArray方法的有noalias的内在函数:
- int Length { get; }
- T this[int index] { get; set; }
任何其他成员的使用都将noalias自动禁用优化。
优化指南
内存别名(Memory Aliasing)和 noalias
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];
}
}
}
翻译不易,转载请注明原译者alph258。
没有内存别名:
如果两个数组Input并Output没有轻微重叠,这意味着它们各自的内存位置没有别名,我们将在示例输入/输出上运行此作业后得到以下结果:
在这里插入图片描述
自动矢量化器没有内存别名:
现在,如果编译器是noalias唤醒,它将能够通过所谓的向量化来优化先前的标量循环(在标量级别工作):编译器将代表您重写循环以按小批量处理元素(工作在矢量级别,例如4乘4个元素)像这样:
在这里插入图片描述
内存别名:
接下来,如果由于某些原因(今天很困难的将JobSystem引入),输出数组实际上是将一个元素与输入数组重叠(例如Output[0]实际指向的点Input[1])意味着内存是别名,我们将得到以下内容运行时的结果CopyJob(假设自动矢量化器没有运行):
在这里插入图片描述
使用无效矢量化代码的内存别名:
更糟糕的是,如果编译器不知道这种内存别名,它仍然会尝试自动向量化循环,我们会得到以下结果,这与以前的标量版本不同:
在这里插入图片描述
此代码的结果将无效,如果编译器未识别它们,则可能导致非常严重的错误。
解决方案burst和JobSystem
为了确保Job可以安全地进行矢量化(当有循环时),burst依赖于:
- JobSystem的安全性的假设您可以在作业中指定输入/输出中的数据:这意味着默认情况下,通过作业安全访问的所有数据都不是别名
- 进一步分析代码的burst,以确保代码也是安全的 burst中的别名分析目前依赖于您的代码需要遵循的一些约束,以便让自动矢量化器正常工作:
- 只有NativeArray被使用,且只有属性Length或索引this[index]被使用
- 不应将本机容器(例如NativeArray)或间接包含容器的结构复制到局部变量
- 本地容器可以通过值传递给方法参数,在所有参数都来自标识源的条件下,这些参数来自静态方法的字段或其他参数,但不是两者都是,并且方法是静态的
- Native Containers或间接包含容器的struct不会存储到struct的字段中
- 假设在“ Jobs”菜单中取消选中“ Enable Burst Safety Checks” 选项
我们期望通过更细粒度的模型来改进别名分析,这将允许放松一些这些约束。
使用noalias分析生成代码的示例
让我们以CopyJob编译到本机代码并禁用noalias分析为例。
以下循环是x64使用启用AVX2的noalias analysis enabled的指令进行编译的结果:(注意我们只复制核心循环,而不是整个方法的完整序言和结尾)
该指令vmovups在这里移动了8个浮点数,因此单个自动向量化循环现在移动4 x 8 = 每个循环迭代复制32个浮点数而不是一个!(因此,相对于与原始循环,将有/ 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
禁用noalias分析的相同循环将每次循环迭代仅复制一个浮点数:
.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
我们可以看到,这里的性能差异很大。这就是为什么noalias 意识到本机代码生成是基础,而这正是burst 试图解决的问题。
编译器选项
编译作业时,可以更改编译器的行为:
- 对数学函数使用不同的精度(sin,cos …)
- 允许编译器通过放宽数学计算的顺序来重新排列浮点计算。
- 强制同步编译作业(仅适用于编辑器/ JIT案例)
- 使用内部编译器选项(尚未详细)
这些标签可以通过使用 [BurstCompile] 属性来设置,比如 [BurstCompile(Accuracy.Med, Support.Relaxed)]
准确性
准确性由以下枚举定义:
public enum Accuracy
{
Std,
Low,
Med,
High,
}
目前,实施仅提供以下准确性:
- Std提供1 ULP的准确度。这是默认值。
- High,Med,Low正在提供3.5 ULP的精度
High对于大多数游戏来说,使用准确度应该足够了。
ULP(最后位置的单位或最小精度的单位)是浮点数之间的间隔,即,一个值的最小精度数字决定了它是否为1.
我们希望支持更多的ULP准确性Med和Low突发的未来版本
编译器放松
编译器松弛由以下枚举定义:
public enum Support
{
Strict,
Relaxed
}
- 严格:编译器没有执行任何计算的重新排列,并且编译器在关注特殊浮点值(非正常,NaN …)时要小心。这是默认值
- 放松:编译器可以执行指令重新排列 和/或 使用 专用或者不太精确 的硬件SIMD指令。
通常,某些硬件可以支持乘法和加法(例如mad a * b + c)到单个指令中。使用宽松计算可以允许这些优化。重新排序这些指令会导致精度降低。
使用Relaxed编译器松弛可以用于许多场景,其中严格要求计算的确切顺序和NaN值的统一处理。
同步编译
默认情况下,编辑器中的burst 编译器是采用异步进行编译作业的。
您可以通过设置CompileSynchronously = true的[BurstCompile]属性改变这种行为:
[BurstCompile(CompileSynchronously = true)]public struct MyJob : IJob
{
// ...
}
Unity.Mathematics
所述Unity.Mathematics提供矢量类型(float4,float3被直接映射到硬件寄存器SIMD …)。
此外,来自math类型的许多功能也直接映射到硬件SIMD指令。
请注意,目前,该库的最佳使用,建议使用SIMD 4位宽类型(float4,int4,bool4…)
独立播放器支持
burst 编译器支持独立播放器。
用法
在构建播放器时,burst将为游戏中的所有突发作业编译单个动态库。根据平台的不同,动态库将输出到不同的文件夹(在Windows上,它位于路径中Data/Plugins/lib_burst_generated.dll)
jobs系统运行时将在由burst编译的第一个作业上加载此库。
启用编译的设置由Jobs/burst 菜单控制,与编辑器相同。
在将来的迭代中,这些设置将被移动到每个平台/播放器的适当设置。
支持的平台
Burst支持以下独立播放器平台:
- 视窗
- MacOS的
- Linux的
- Xbox One
- PS4
- Android(ARM v7和v8 +)
- iOS(ARM v7和v8 +)
已知的问题
- 目前不支持精度/精度
- 目标CPU当前是每个平台的硬编码(例如,Windows 64位的SSE4)
这些已知问题将在未来的burst版本中得到解决。
网友评论