A game about forced loneliness, made by TACStudios
at master 179 lines 8.8 kB view raw view rendered
1# Function pointers 2 3To work with dynamic functions that process data based on other data states, use [`FunctionPointer<T>`](xref:Unity.Burst.FunctionPointer`1). Because Burst treats delegates as managed objects, you can't use [C# delegates](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/delegates/) to work with dynamic functions. 4 5## Support details 6 7Function pointers don't support generic delegates. Also, avoid wrapping [`BurstCompiler.CompileFunctionPointer<T>`](xref:Unity.Burst.BurstCompiler.CompileFunctionPointer``1(``0)) within another open generic method. If you do this, Burst can't apply required attributes to the delegate, perform additional safety analysis, or perform potential optimizations. 8 9Argument and return types are subject to the same restrictions as `DllImport` and internal calls. For more information, see the documentation on [DllImport and internal calls](csharp-burst-intrinsics-dllimport.md). 10 11### Interoperability with IL2CPP 12 13Interoperability of function pointers with IL2CPP requires `System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute` on the delegate. Set the calling convention to `CallingConvention.Cdecl`. Burst automatically adds this attribute to delegates that are used with [`BurstCompiler.CompileFunctionPointer<T>`](xref:Unity.Burst.BurstCompiler.CompileFunctionPointer``1(``0)). 14 15## Using function pointers 16 17To use function pointers, identify the static functions that you want Burst to compile and do the following: 18 191. Add a `[BurstCompile]` attribute to these functions 201. Add a `[BurstCompile]` attribute to the containing type. This helps the Burst compiler find the static methods that have `[BurstCompile]` attribute 211. Declare a delegate to create the "interface" of these functions 221. Add a `[MonoPInvokeCallbackAttribute]` attribute to the functions. You need to add this so that IL2CPP works with these functions. For example: 23 24 ```c# 25 // Instruct Burst to look for static methods with [BurstCompile] attribute 26 [BurstCompile] 27 class EnclosingType { 28 [BurstCompile] 29 [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] 30 public static float MultiplyFloat(float a, float b) => a * b; 31 32 [BurstCompile] 33 [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] 34 public static float AddFloat(float a, float b) => a + b; 35 36 // A common interface for both MultiplyFloat and AddFloat methods 37 public delegate float Process2FloatsDelegate(float a, float b); 38 } 39 ``` 40 411. Compile these function pointers from regular C# code: 42 43 ```c# 44 // Contains a compiled version of MultiplyFloat with Burst 45 FunctionPointer<Process2FloatsDelegate> mulFunctionPointer = BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat); 46 47 // Contains a compiled version of AddFloat with Burst 48 FunctionPointer<Process2FloatsDelegate> addFunctionPointer = BurstCompiler. CompileFunctionPointer<Process2FloatsDelegate>(AddFloat); 49 ``` 50 51### Using function pointers in a job 52 53To use the function pointers directly from a job, pass them to the job struct: 54 55```c# 56 // Invoke the function pointers from HPC# jobs 57 var resultMul = mulFunctionPointer.Invoke(1.0f, 2.0f); 58 var resultAdd = addFunctionPointer.Invoke(1.0f, 2.0f); 59``` 60 61Burst compiles function pointers asynchronously for jobs by default. To force a synchronous compilation of function pointers use `[BurstCompile(SynchronousCompilation = true)]`. 62 63### Using function pointers in C# code 64 65To use these function pointers from regular C# code, cache the `FunctionPointer<T>.Invoke` property (which is the delegate instance) to a static field to get the best performance: 66 67```c# 68 private readonly static Process2FloatsDelegate mulFunctionPointerInvoke = BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat).Invoke; 69 70 // Invoke the delegate from C# 71 var resultMul = mulFunctionPointerInvoke(1.0f, 2.0f); 72``` 73 74Using Burst-compiled function pointers from C# might be slower than their pure C# version counterparts if the function is too small compared to the overhead of [`P/Invoke`](https://docs.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke) interop. 75 76 77## Performance considerations 78 79Where possible, you use a job over a function pointer to run Burst compiled code, because jobs are more optimal. Burst provides better aliasing calculations for jobs because the job safety system has more optimizations by default. 80 81You also can't pass most of the `[NativeContainer]` structs like `NativeArray` directly to function pointers and must use a job struct to do so. Native container structs contain managed objects for safety checks that the Burst compiler can work around when compiling jobs, but not for function pointers. 82 83The following example shows a bad example of how to use function pointers in Burst. The function pointer computes `math.sqrt` from an input pointer and stores it to an output pointer. `MyJob` feeds this function pointer sources from two `NativeArray`s which isn't optimal: 84 85```c# 86///Bad function pointer example 87[BurstCompile] 88public class MyFunctionPointers 89{ 90 public unsafe delegate void MyFunctionPointerDelegate(float* input, float* output); 91 92 [BurstCompile] 93 public static unsafe void MyFunctionPointer(float* input, float* output) 94 { 95 *output = math.sqrt(*input); 96 } 97} 98 99[BurstCompile] 100struct MyJob : IJobParallelFor 101{ 102 public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer; 103 104 [ReadOnly] public NativeArray<float> Input; 105 [WriteOnly] public NativeArray<float> Output; 106 107 public unsafe void Execute(int index) 108 { 109 var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr(); 110 var outputPtr = (float*)Output.GetUnsafePtr(); 111 FunctionPointer.Invoke(inputPtr + index, outputPtr + index); 112 } 113} 114``` 115 116This example isn't optimal for the following reasons: 117 118* Burst can't vectorize the function pointer because it's being fed a single scalar element. This means that 4-8x performance is lost from a lack of vectorization. 119* The `MyJob` knows that the `Input` and `Output` native arrays can't alias, but this information isn't communicated to the function pointer. 120* There is a non-zero overhead to constantly branching to a function pointer somewhere else in memory. 121 122To use a function pointer in an optimal way, always process batches of data in the function pointer, like so: 123 124```c# 125[BurstCompile] 126public class MyFunctionPointers 127{ 128 public unsafe delegate void MyFunctionPointerDelegate(int count, float* input, float* output); 129 130 [BurstCompile] 131 public static unsafe void MyFunctionPointer(int count, float* input, float* output) 132 { 133 for (int i = 0; i < count; i++) 134 { 135 output[i] = math.sqrt(input[i]); 136 } 137 } 138} 139 140[BurstCompile] 141struct MyJob : IJobParallelForBatch 142{ 143 public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer; 144 145 [ReadOnly] public NativeArray<float> Input; 146 [WriteOnly] public NativeArray<float> Output; 147 148 public unsafe void Execute(int index, int count) 149 { 150 var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr() + index; 151 var outputPtr = (float*)Output.GetUnsafePtr() + index; 152 FunctionPointer.Invoke(count, inputPtr, outputPtr); 153 } 154} 155``` 156 157Thee modified `MyFunctionPointer` takes a `count` of elements to process, and loops over the `input` and `output` pointers to do a lot of calculations. The `MyJob` becomes an `IJobParallelForBatch`, and the `count` is passed directly into the function pointer. This is better for performance because of the following reasons: 158 159* Burst vectorizes the `MyFunctionPointer` call. 160* Because Burst processes `count` items per function pointer, any overhead of calling the function pointer is reduced by `count` times. For example, if you run a batch of 128, the function pointer overhead is 1/128th per `index` of what it was previously. 161* Batching results in a 1.53x performance gain over not batching. 162 163However, to get the best possible performance, use a job. This gives Burst the most visibility over what you want it to do, and the most opportunities to optimize: 164 165```c# 166[BurstCompile] 167struct MyJob : IJobParallelFor 168{ 169 [ReadOnly] public NativeArray<float> Input; 170 [WriteOnly] public NativeArray<float> Output; 171 172 public unsafe void Execute(int index) 173 { 174 Output[i] = math.sqrt(Input[i]); 175 } 176} 177``` 178 179This runs 1.26x faster than the batched function pointer example, and 1.93x faster than the non-batched function pointer examples. Burst has perfect aliasing knowledge and can make the broadest modifications to the above. This code is also a lot simpler than either of the function pointer cases.