How do Out, In, Ref params work with Burst?

I looked at Burst docs and it says that params on static functions follow the convention of Function Pointers. But I’m not sure if I’m reading it right…

Which and how parameter types work with Bursted static methods?

Out seems to be working, because it is used in Burst examples in the docs… but seems like a wrapper.

In . Not sure if variables are copied or not. Seems like if you pass a struct, it’s passed by “readonly ref” as expected. But some optimizations decide if a copy would be better or not. Does Burst do that? Can we use this in Burst? I would use readonly ref… but it’s not available yet.

ref this one works.

1 Like

I don’t know anything about Burst, but the “in” modifier sometimes results in defensive copies of the struct being created. I was trying to find an official list of rules around exactly when that does and does not occur, but maybe these articles will help. The rule of thumb as I understand it is that anything about a struct that is not readonly could result in a defensive copy of the struct being created if passed “in”.

https://devblogs.microsoft.com/premier-developer/the-in-modifier-and-the-readonly-structs-in-c/
https://devblogs.microsoft.com/premier-developer/performance-traps-of-ref-locals-and-ref-returns-in-c/
https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/

2 Likes

So although the general advice for C# code might be to only use in parameters with readonly structs, in Bursted HPC# as long as you do not store into the in parameter data you should be fine.

1 Like

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
1 Like