Normally Burst-compiled jobs can’t use managed arrays, but there’s an exception for static readonly fields. This comes with several dangers, which we’ll explore today.

Update: A Russian translation of this article is available.

As Intended

Let’s start by looking at the intended use of static readonly managed array fields in Burst: lookup tables. Here’s a simple job that reads an integer out of the array:

[BurstCompile]
struct ReadManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Result[0] = Array[Index];
    }
}

Using the Burst Inspector, we see this x86 assembly code is generated:

mov     rax, qword ptr [rdi + 8]
movsxd  rcx, dword ptr [rdi]
movabs  rdx, offset ".LSystem.Int32[] ReadManagedArrayJob::Array"
mov     ecx, dword ptr [rdx + 4*rcx]
mov     dword ptr [rax], ecx

This is just indexing into the executable’s data section called .LSystem.Int32[] ReadManagedArrayJob::Array, which we see below:

".LSystem.Int32[] ReadManagedArrayJob::Array":
    .long    1
    .long    2
    .long    3
    .size    ".LSystem.Int32[] ReadManagedArrayJob::Array", 12
    .section .debug_str,"MS",@progbits,1

Notice how the section contains exactly what we put in the array: 1, 2, and 3.

Writing to the Array

Now let’s see what happens if we break the rules and try to write to the managed array:

[BurstCompile]
struct WriteManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Array[Index] = Result[0];
    }
}

The Console pane in Unity shows no errors, but Burst Inspector shows us a compiler error:

/path/to/project/Assets/TestScript.cs(31,3): error: Accessing a managed array is not supported by burst
 at WriteManagedArrayJob.Execute(WriteManagedArrayJob* this) (at /path/to/project/Assets/TestScript.cs:31)
 at Unity.Jobs.IJobExtensions.JobStruct`1<WriteManagedArrayJob>.Execute(ref WriteManagedArrayJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<WriteManagedArrayJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

We’ll get the same error if we try to build the project, but in the meantime it’s very easy to go on using the job in the editor without realizing there’s a serious problem. On a less serious note, the error message isn’t very accurate as accessing a managed array is supported by Burst, just not in the way this code is trying to access it by writing.

Passing Arrays

Next, let’s break another rule and try to pass the array as a function parameter:

[BurstCompile]
struct PassManagedArrayJob : IJob
{
    public static readonly int[] Array = { 1, 2, 3 };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Do(Array);
    }
 
    public void Do(int[] a)
    {
        Result[0] = a[Index];
    }
}

This also has no error in the Console pane, but shows an error in the Burst Inspector and upon building the project:

/path/to/project/Assets/TestScript.cs(45,3): error: The managed function `PassManagedArrayJob.Do(PassManagedArrayJob* this, int[] a)` is not supported by burst
 at PassManagedArrayJob.Execute(PassManagedArrayJob* this) (at /path/to/project/Assets/TestScript.cs:45)
 at Unity.Jobs.IJobExtensions.JobStruct`1<PassManagedArrayJob>.Execute(ref PassManagedArrayJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<PassManagedArrayJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

This error message doesn’t say why the function isn’t supported by Burst, but it should be somewhat apparant from the function signature that its use of a managed array isn’t allowed.

Writing From Outside

Now let’s break the rule that says we shouldn’t modify the array from outside the job:

class TestScript : MonoBehaviour
{
    void Start()
    {
        ReadManagedArrayJob.Array[0] = 100;
        NativeArray<int> array = new NativeArray<int>(1, Allocator.TempJob);
        new ReadManagedArrayJob { Result = array, Index = 0 }.Run();
        print(array[0]);
        array.Dispose();
    }
}

Here we’ve written over the 1 at the first element of the array and then proceeded to run the job to read the first element of the array and print it. What does it print?

100

Having seen the assembly output above, this result doesn’t make sense. The job should be reading from a compile-time array stored in the executable’s data section. How can it be modified at runtime by TestScript.Start?

At this point, we build the project and run it on macOS to get this result:

1

Now we see the opposite behavior! The Burst-compiled job that’s used in the build of the project differs in functionality from the version of the job that’s used in the editor. Again, we may go for some time without realizing our mistake. Worst of all, there are no compiler warnings or errors in the editor or during the build to alert us to the problem. Beware!

Array of Structs

Now let’s try using an array of structs:

struct TestStruct
{
    public int Value;
 
    public TestStruct(int value)
    {
        Value = value;
    }
}
 
[BurstCompile]
struct ManagedArrayOfStructsJob : IJob
{
    public static readonly TestStruct[] Array = {
        new TestStruct(1),
        new TestStruct(2)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Result[0] = Array[Index].Value;
    }
}

This is functionally equivalent to ReadManagedArrayJob and so we see the exact same assembly in Burst Inspector:

mov     rax, qword ptr [rdi + 8]
movsxd  rcx, dword ptr [rdi]
movabs  rdx, offset ".LTestStruct[] ManagedArrayOfStructsJob::Array"
mov     ecx, dword ptr [rdx + 4*rcx]
mov     dword ptr [rax], ecx

The data section also looks the same:

".LTestStruct[] ManagedArrayOfStructsJob::Array":
    .long    1
    .long    2
    .size    ".LTestStruct[] ManagedArrayOfStructsJob::Array", 8
    .section .debug_str,"MS",@progbits,1

Notice that there’s no overhead for structs and no cost to read from them.

Structs with Conditionals

Let’s go back to breaking the rules and use a conditional, a ternary operator in this case, inside our struct’s constructor:

struct TestStructWithConditional
{
    public int Value;
 
    public TestStructWithConditional(bool use1)
    {
        Value = use1 ? 1 : 2;
    }
}
 
[BurstCompile]
struct ManagedArrayOfStructsWithConditionalJob : IJob
{
    public static readonly TestStructWithConditional[] Array = {
        new TestStructWithConditional(true),
        new TestStructWithConditional(false)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Result[0] = Array[Index].Value;
    }
}

Burst Inspector now gives four errors:

/path/to/project/Assets/TestScript.cs(104,3): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:104)
 at Unity.Jobs.IJobExtensions.JobStruct`1<ManagedArrayOfStructsWithConditionalJob>.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<ManagedArrayOfStructsWithConditionalJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Creating a managed array `TestStructWithConditional[]` is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1<ManagedArrayOfStructsWithConditionalJob>.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<ManagedArrayOfStructsWithConditionalJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1<ManagedArrayOfStructsWithConditionalJob>.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<ManagedArrayOfStructsWithConditionalJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)/path/to/project/Assets/TestScript.cs(94,2): error: Accessing a managed array is not supported by burst
 at ManagedArrayOfStructsWithConditionalJob..cctor() (at /path/to/project/Assets/TestScript.cs:94)
 at ManagedArrayOfStructsWithConditionalJob.Execute(ManagedArrayOfStructsWithConditionalJob* this) (at /path/to/project/Assets/TestScript.cs:103)
 at Unity.Jobs.IJobExtensions.JobStruct`1<ManagedArrayOfStructsWithConditionalJob>.Execute(ref ManagedArrayOfStructsWithConditionalJob data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref Unity.Jobs.LowLevel.Unsafe.JobRanges ranges, int jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
 
 
While compiling job: System.Void Unity.Jobs.IJobExtensions/JobStruct`1<ManagedArrayOfStructsWithConditionalJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

Three of these repeat the inaccuracy that managed arrays can’t be accessed under Burst and the last introduces another inaccuracy in saying that managed arrays can’t be created by Burst. Clearly they can, as we have, but not when their elements are structs with conditional code.

Again, we don’t get errors in the Console pane of the editor. These errors only appear in Burst Inspector or when building the project, so they may go unnoticed.

Structs with Exceptions

Lastly, let’s try throwing an exception in a struct constructor:

struct TestStructWithException
{
    public int Value;
 
    public TestStructWithException(int value)
    {
        Value = value;
        throw new Exception("boom");
    }
}
 
[BurstCompile]
struct ManagedArrayOfStructsWithExceptionJob : IJob
{
    public static readonly TestStructWithException[] Array = {
        new TestStructWithException(1),
        new TestStructWithException(2)
    };
 
    public int Index;
    public NativeArray<int> Result;
 
    public void Execute()
    {
        Result[0] = Array[Index].Value;
    }
}

We might expect the same result as with the conditionals, but we’d be wrong:

        mov     rax, qword ptr [rdi + 8]
        movsxd  rcx, dword ptr [rdi]
        movabs  rdx, offset ".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array"
        mov     ecx, dword ptr [rdx + 4*rcx]
        mov     dword ptr [rax], ecx

And here’s the data section

".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array":
    .long    1
    .long    2
    .size    ".LTestStructWithException[] ManagedArrayOfStructsWithExceptionJob::Array", 8
    .type    .Lburst_abort.function.string,@object
    .section .rodata,"a",@progbits

All trace of the exception has been removed from the output. It’s as though we didn’t write the throw line at all.

So what happens if we try to run this?

class TestScript : MonoBehaviour
{
    void Start()
    {
        NativeArray<int> array = new NativeArray<int>(1, Allocator.TempJob);
        new ManagedArrayOfStructsWithExceptionJob { Result = array, Index = 0 }.Run();
        print(array[0]);
        array.Dispose();
    }
}

In the editor, we get the exception:

Exception: boom
TestStructWithException..ctor (System.Int32 value) (at Assets/TestScript.cs:115)
ManagedArrayOfStructsWithExceptionJob..cctor () (at Assets/TestScript.cs:122)
Rethrow as TypeInitializationException: The type initializer for 'ManagedArrayOfStructsWithExceptionJob' threw an exception.
Unity.Jobs.IJobExtensions+JobStruct`1[T].Execute (T& data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:30)
Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule_Injected(JobScheduleParameters&, JobHandle&)
Unity.Jobs.LowLevel.Unsafe.JobsUtility:Schedule(JobScheduleParameters&)
Unity.Jobs.IJobExtensions:Run(ManagedArrayOfStructsWithExceptionJob) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:43)
TestScript:Start() (at Assets/TestScript.cs:147)

In a macOS build, we get a result:

1

Again, there were no compiler warnings or errors given in the editor when it compiled the code, when it ran it, or when it built the project. Beware!

Conclusion

Managed arrays can be used in Burst, but with many caveats. Those caveats are enforced inconsistently. Sometimes there are compiler errors when making a build or looking in Burst Inspector, but sometimes not. Sometimes the behavior in the editor and in a build are different, with no indication from Unity that a problem is present in the job code. So it’s important that you know the rules of managed arrays in Burst and that you employ a good degree of discipline when using them!