This article is pretty great, but refers to in parameters as used in the body of a compiled function or job. As far as I understand, OP is interested specifically in parameter passing at the function call boundary in case of Burst-compiled static functions.
I’m not super qualified to answer, so maybe someone will correct me here.
As far as I can tell, all three of the in
, ref
and out
parameter modifiers all result in the parameters being passed by pointer to the Burst-compiled function. I don’t think there’s any actual difference to how the parameter is handled by the Burst compiler. So since you’re writing your code in C#, the difference seems to be primarily in what code Roslyn considers valid. On the Burst side, it’s all pointers anyway.
I made a toy example, not sure how well it scales to real world cases. (I used pointers to write to the in
parameter - that’s evil, don’t do that.)
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public static class BurstTest
{
[BurstCompile]
public static unsafe void TestInParam(in float3 value)
{
fixed (float3* ptr = &value)
*ptr = new(1, 2, 3);
}
[BurstCompile]
public static void TestRefParam(ref float3 value)
{
value = new(1, 2, 3);
}
[BurstCompile]
public static void TestOutParam(out float3 value)
{
value = new(1, 2, 3);
}
}
These three functions compile down to pretty much the same native code. The only differences seem to be in some identifiers, and in the order of the output. (I rearranged some of the assembly to make the diff cleaner.)
TestRefParam
diffed with TestInParam
:
diff --git TestRefParam TestInParam
index 5bffce1..3f1a4dc 100644
--- TestRefParam
+++ TestInParam
@@ -1,86 +1,86 @@
.text
.def @feat.00;
.scl 3;
.type 0;
.endef
.globl @feat.00
.set @feat.00, 0
.intel_syntax noprefix
.file "main"
- .def 8490770cabcb814e357e4c1e5a5deb61;
+ .def burst.initialize;
.scl 2;
.type 32;
.endef
- .globl 8490770cabcb814e357e4c1e5a5deb61
+ .globl burst.initialize
.p2align 4, 0x90
-8490770cabcb814e357e4c1e5a5deb61:
-.seh_proc 8490770cabcb814e357e4c1e5a5deb61
+cf4272599c3ad1b1c27cb0e52b4e7e2d:
+.seh_proc cf4272599c3ad1b1c27cb0e52b4e7e2d
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
movabs rax, 4611686019492741120
mov qword ptr [rcx], rax
mov dword ptr [rcx + 8], 1077936128
pop rbp
ret
.seh_endproc
- .def burst.initialize;
+ .def cf4272599c3ad1b1c27cb0e52b4e7e2d;
.scl 2;
.type 32;
.endef
- .globl burst.initialize
+ .globl cf4272599c3ad1b1c27cb0e52b4e7e2d
.p2align 4, 0x90
burst.initialize:
.seh_proc burst.initialize
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.def burst.initialize.externals;
.scl 2;
.type 32;
.endef
.globl burst.initialize.externals
.p2align 4, 0x90
burst.initialize.externals:
.seh_proc burst.initialize.externals
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.def burst.initialize.statics;
.scl 2;
.type 32;
.endef
.globl burst.initialize.statics
.p2align 4, 0x90
burst.initialize.statics:
.seh_proc burst.initialize.statics
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.section .drectve,"yni"
- .ascii " /EXPORT:8490770cabcb814e357e4c1e5a5deb61"
.ascii " /EXPORT:\"burst.initialize\""
.ascii " /EXPORT:\"burst.initialize.externals\""
.ascii " /EXPORT:\"burst.initialize.statics\""
+ .ascii " /EXPORT:cf4272599c3ad1b1c27cb0e52b4e7e2d"
.globl _fltused
TestRefParam
diffed with TestOutParam
:
diff --git TestRefParam TestOutParam
index 5bffce1..8353e45 100644
--- TestRefParam
+++ TestOutParam
@@ -1,86 +1,86 @@
.text
.def @feat.00;
.scl 3;
.type 0;
.endef
.globl @feat.00
.set @feat.00, 0
.intel_syntax noprefix
.file "main"
- .def 8490770cabcb814e357e4c1e5a5deb61;
+ .def 77ecc84255bff7783f7d8cc55f3f1c4c;
.scl 2;
.type 32;
.endef
- .globl 8490770cabcb814e357e4c1e5a5deb61
+ .globl 77ecc84255bff7783f7d8cc55f3f1c4c
.p2align 4, 0x90
-8490770cabcb814e357e4c1e5a5deb61:
-.seh_proc 8490770cabcb814e357e4c1e5a5deb61
+77ecc84255bff7783f7d8cc55f3f1c4c:
+.seh_proc 77ecc84255bff7783f7d8cc55f3f1c4c
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
movabs rax, 4611686019492741120
mov qword ptr [rcx], rax
mov dword ptr [rcx + 8], 1077936128
pop rbp
ret
.seh_endproc
.def burst.initialize;
.scl 2;
.type 32;
.endef
.globl burst.initialize
.p2align 4, 0x90
burst.initialize:
.seh_proc burst.initialize
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.def burst.initialize.externals;
.scl 2;
.type 32;
.endef
.globl burst.initialize.externals
.p2align 4, 0x90
burst.initialize.externals:
.seh_proc burst.initialize.externals
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.def burst.initialize.statics;
.scl 2;
.type 32;
.endef
.globl burst.initialize.statics
.p2align 4, 0x90
burst.initialize.statics:
.seh_proc burst.initialize.statics
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
.seh_endprologue
pop rbp
ret
.seh_endproc
.section .drectve,"yni"
- .ascii " /EXPORT:8490770cabcb814e357e4c1e5a5deb61"
+ .ascii " /EXPORT:77ecc84255bff7783f7d8cc55f3f1c4c"
.ascii " /EXPORT:\"burst.initialize\""
.ascii " /EXPORT:\"burst.initialize.externals\""
.ascii " /EXPORT:\"burst.initialize.statics\""
.globl _fltused
Finally, there’s the question about how the Burst function call actually works. We can decompile the .NET assembly to learn that Burst moves the body of the function to a new function holding the managed implementation, and rewrites the original function to either invoke the managed implementation, or the Burst-compiled implementation, depending on whether Burst is enabled at runtime.
using AOT;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public static class BurstTest
{
[BurstCompile]
public static void TestInParam([IsReadOnly] in float3 value)
{
BurstTest.TestInParam_00000005\u0024BurstDirectCall.Invoke(in value);
}
[BurstCompile]
public static void TestRefParam(ref float3 value)
{
BurstTest.TestRefParam_00000006\u0024BurstDirectCall.Invoke(ref value);
}
[BurstCompile]
public static void TestOutParam(out float3 value)
{
BurstTest.TestOutParam_00000007\u0024BurstDirectCall.Invoke(out value);
}
[BurstCompile]
[MonoPInvokeCallback(typeof (BurstTest.TestInParam_00000005\u0024PostfixBurstDelegate))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void TestInParam\u0024BurstManaged([IsReadOnly] in float3 value)
{
fixed (float3* float3Ptr = &value)
*float3Ptr = new float3(1f, 2f, 3f);
}
[BurstCompile]
[MonoPInvokeCallback(typeof (BurstTest.TestRefParam_00000006\u0024PostfixBurstDelegate))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void TestRefParam\u0024BurstManaged(ref float3 value)
{
value = new float3(1f, 2f, 3f);
}
[BurstCompile]
[MonoPInvokeCallback(typeof (BurstTest.TestOutParam_00000007\u0024PostfixBurstDelegate))]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void TestOutParam\u0024BurstManaged(out float3 value)
{
value = new float3(1f, 2f, 3f);
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void TestInParam_00000005\u0024PostfixBurstDelegate([IsReadOnly] in float3 value);
internal static class TestInParam_00000005\u0024BurstDirectCall
{
private static IntPtr Pointer;
[BurstDiscard]
private static void GetFunctionPointerDiscard([In] ref IntPtr obj0)
{
if (BurstTest.TestInParam_00000005\u0024BurstDirectCall.Pointer == IntPtr.Zero)
BurstTest.TestInParam_00000005\u0024BurstDirectCall.Pointer = BurstCompiler.CompileFunctionPointer<BurstTest.TestInParam_00000005\u0024PostfixBurstDelegate>(new BurstTest.TestInParam_00000005\u0024PostfixBurstDelegate((object) null, __methodptr(TestInParam\u0024BurstManaged))).Value;
obj0 = BurstTest.TestInParam_00000005\u0024BurstDirectCall.Pointer;
}
private static IntPtr GetFunctionPointer()
{
IntPtr zero = IntPtr.Zero;
BurstTest.TestInParam_00000005\u0024BurstDirectCall.GetFunctionPointerDiscard(ref zero);
return zero;
}
public static void Invoke([IsReadOnly] in float3 value)
{
if (BurstCompiler.IsEnabled)
{
IntPtr functionPointer = BurstTest.TestInParam_00000005\u0024BurstDirectCall.GetFunctionPointer();
if (functionPointer != IntPtr.Zero)
{
ref readonly float3 local = ref value;
__calli((__FnPtr<void (float3&)>) functionPointer)(ref local);
return;
}
}
BurstTest.TestInParam\u0024BurstManaged(in value);
}
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void TestRefParam_00000006\u0024PostfixBurstDelegate(ref float3 value);
internal static class TestRefParam_00000006\u0024BurstDirectCall
{
private static IntPtr Pointer;
[BurstDiscard]
private static void GetFunctionPointerDiscard([In] ref IntPtr obj0)
{
if (BurstTest.TestRefParam_00000006\u0024BurstDirectCall.Pointer == IntPtr.Zero)
BurstTest.TestRefParam_00000006\u0024BurstDirectCall.Pointer = BurstCompiler.CompileFunctionPointer<BurstTest.TestRefParam_00000006\u0024PostfixBurstDelegate>(new BurstTest.TestRefParam_00000006\u0024PostfixBurstDelegate((object) null, __methodptr(TestRefParam\u0024BurstManaged))).Value;
obj0 = BurstTest.TestRefParam_00000006\u0024BurstDirectCall.Pointer;
}
private static IntPtr GetFunctionPointer()
{
IntPtr zero = IntPtr.Zero;
BurstTest.TestRefParam_00000006\u0024BurstDirectCall.GetFunctionPointerDiscard(ref zero);
return zero;
}
public static void Invoke(ref float3 value)
{
if (BurstCompiler.IsEnabled)
{
IntPtr functionPointer = BurstTest.TestRefParam_00000006\u0024BurstDirectCall.GetFunctionPointer();
if (functionPointer != IntPtr.Zero)
{
ref float3 local = ref value;
__calli((__FnPtr<void (float3&)>) functionPointer)(ref local);
return;
}
}
BurstTest.TestRefParam\u0024BurstManaged(ref value);
}
}
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void TestOutParam_00000007\u0024PostfixBurstDelegate(out float3 value);
internal static class TestOutParam_00000007\u0024BurstDirectCall
{
private static IntPtr Pointer;
[BurstDiscard]
private static void GetFunctionPointerDiscard([In] ref IntPtr obj0)
{
if (BurstTest.TestOutParam_00000007\u0024BurstDirectCall.Pointer == IntPtr.Zero)
BurstTest.TestOutParam_00000007\u0024BurstDirectCall.Pointer = BurstCompiler.CompileFunctionPointer<BurstTest.TestOutParam_00000007\u0024PostfixBurstDelegate>(new BurstTest.TestOutParam_00000007\u0024PostfixBurstDelegate((object) null, __methodptr(TestOutParam\u0024BurstManaged))).Value;
obj0 = BurstTest.TestOutParam_00000007\u0024BurstDirectCall.Pointer;
}
private static IntPtr GetFunctionPointer()
{
IntPtr zero = IntPtr.Zero;
BurstTest.TestOutParam_00000007\u0024BurstDirectCall.GetFunctionPointerDiscard(ref zero);
return zero;
}
public static void Invoke(out float3 value)
{
if (BurstCompiler.IsEnabled)
{
IntPtr functionPointer = BurstTest.TestOutParam_00000007\u0024BurstDirectCall.GetFunctionPointer();
if (functionPointer != IntPtr.Zero)
{
ref float3 local = ref value;
__calli((__FnPtr<void (float3&)>) functionPointer)(ref local);
return;
}
}
BurstTest.TestOutParam\u0024BurstManaged(out value);
}
}
}
To me it looks like the in
/ref
/out
modifiers are treated the same way on the Mono side already. The decompiler seems to get confused a little, emitting some illegal C# (ref
access to an in
/out
parameter), and using a raw calli
IL instruction to call the function pointer. (Emitting raw IL lets you get away with all kinds of stuff.)
Here’s the IL if you’re still hungry:
.method public hidebysig static void
Invoke(
[in] valuetype [Unity.Mathematics]Unity.Mathematics.float3& 'value'
) cil managed
{
.param [1]
.custom instance void [netstandard]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor()
= (01 00 00 00 )
.maxstack 2
.locals init (
[0] native int V_0
)
IL_0000: call bool [Unity.Burst]Unity.Burst.BurstCompiler::get_IsEnabled()
IL_0005: brfalse IL_001e
IL_000a: call native int BurstTest/TestInParam_00000005$BurstDirectCall::GetFunctionPointer()
IL_000f: stloc.0 // V_0
IL_0010: ldloc.0 // V_0
IL_0011: brfalse IL_001e
IL_0016: ldarg.0 // 'value'
IL_0017: ldloc.0 // V_0
IL_0018: calli void (valuetype [Unity.Mathematics]Unity.Mathematics.float3&)
IL_001d: ret
IL_001e: ldarg.0 // 'value'
IL_001f: call void BurstTest::TestInParam$BurstManaged(valuetype [Unity.Mathematics]Unity.Mathematics.float3&)
IL_0024: ret
} // end of method TestInParam_00000005$BurstDirectCall::Invoke