Tuples are a new feature in C# 7 and they’re backed by the ValueTuple struct, not class. Hopefully they’ll be supported by Burst, so let’s try them out!

C# Tuples

The Burst manual doesn’t explicitly mention tuples, so we’ll just have to try them out and see if they work. To do so, let’s write a little job that takes a tuple and a NativeArray of tuples and assigns the tuple to every element of the array:

[BurstCompile]
public struct TupleJob : IJob
{
    public (int, int, int) Tuple;
    public NativeArray<(int, int, int)> Array;
 
    public void Execute()
    {
        for (int i = 0; i < Array.Length; ++i)
        {
            Array[i] = Tuple;
        }
    }
}

The tuple part here is where we use (int, int, int) as a type. The parentheses indicate a tuple and the types within it indicate the elements of the tuple struct.

Now let’s run the job like this:

(int, int, int) tuple = (1, 2, 3);
NativeArray<(int, int, int)> array = new NativeArray<(int, int, int)>(
    1,
    Allocator.TempJob);
new TupleJob {Tuple = tuple, Array = array}.Run();
print(array[0]);
array.Dispose();

Here we see the creation of a tuple with (1, 2, 3). The parentheses again indicate a tuple and the comma-delimited values indicate the values to assign to the struct’s fields.

Now let’s look at the Burst Inspector to see the compliation results for the job:

/Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs(145,13): error: Struct `System.ValueTuple`3` with auto layout is not supported by burst
 at Unity.Collections.NativeArray`1<System.ValueTuple`3<System.Int32,System.Int32,System.Int32>>.set_Item(Unity.Collections.NativeArray`1<System.ValueTuple`3<int,int,int>>* this, int index, System.ValueTuple`3<int,int,int> value) (at /Users/builduser/buildslave/unity/build/Runtime/Export/NativeArray/NativeArray.cs:145)
 at TupleJob.Execute(TupleJob* this) (at /Users/jackson/Code/UnityPlayground/Assets/TestScript.cs:19)
 at Unity.Jobs.IJobExtensions.JobStruct`1<TupleJob>.Execute(ref TupleJob 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<TupleJob>::Execute(T&,System.IntPtr,System.IntPtr,Unity.Jobs.LowLevel.Unsafe.JobRanges&,System.Int32)

First we notice that the tuple syntax of (int, int, int) has been compiled down to System.ValueTuple`3. That’s .NET’s way of saying System.ValueTuple&lt;T1, T2, T3&gt; because the 3 indicates three generic parameters. It also adds the System namespace for completeness.

Unfortunately, we’re also looking at an error message telling us that this type isn’t supported by Burst. There’s a hint that it’s due to its “auto layout” which isn’t apparent from the Microsoft overview doc or the API docs which show other attributes like [Serializable].

The “auto layout” referred to here presumably references the use of [StructLayout] with LayoutKind.Auto. This is indeed the case if we look up Microsoft’s C# reference source. The Struct types section of the Burst manual says that Sequential and Explicit are supported and Pack is not, but is silent on Auto. Apparently, it is not supported.

Workaround

So, if we want to use tuples we are left in need of a workaround. Thankfully, ValueTuple isn’t a very complex type so it’s easy for us to implement one like so:

using System;
using System.Collections;
using System.Collections.Generic;
 
[Serializable]
public struct MyValueTuple<T1, T2, T3>
    : IStructuralComparable,
        IStructuralEquatable,
        IComparable,
        IComparable<MyValueTuple<T1, T2, T3>>,
        IEquatable<MyValueTuple<T1, T2, T3>>
{
    public T1 Item1;
    public T2 Item2;
    public T3 Item3;
 
    public MyValueTuple(T1 item1, T2 item2, T3 item3)
    {
        Item1 = item1;
        Item2 = item2;
        Item3 = item3;
    }
 
    public int CompareTo(MyValueTuple<T1, T2, T3> other)
    {
        if (Comparer<T1>.Default.Compare(Item1, other.Item1) != 0)
        {
            return Comparer<T1>.Default.Compare(Item1, other.Item1);
        }
 
        if (Comparer<T2>.Default.Compare(Item2, other.Item2) != 0)
        {
            return Comparer<T1>.Default.Compare(Item1, other.Item1);
        }
 
        return Comparer<T3>.Default.Compare(Item3, other.Item3);
    }
 
    public override bool Equals(object obj)
    {
        if (!(obj is MyValueTuple<T1, T2, T3>))
        {
            return false;
        }
 
        MyValueTuple<T1, T2, T3> other = (MyValueTuple<T1, T2, T3>) obj;
        return EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item1,
                   other.Item1)
               && EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item2,
                   other.Item2)
               && EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item3,
                   other.Item3);
    }
 
    public bool Equals(MyValueTuple<T1, T2, T3> other)
    {
        return EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item1,
                   other.Item1)
               && EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item2,
                   other.Item2)
               && EqualityComparer<MyValueTuple<T1, T2, T3>>.Equals(
                   Item3,
                   other.Item3);
    }
 
    private static readonly int randomSeed = new Random().Next(
        int.MinValue,
        int.MaxValue);
 
    private static int Combine(int h1, int h2)
    {
        // RyuJIT optimizes this to use the ROL instruction
        // Related GitHub pull request: dotnet/coreclr#1830
        uint rol5 = ((uint) h1 << 5) | ((uint) h1 >> 27);
        return ((int) rol5 + h1) ^ h2;
    }
 
    private static int CombineHashCodes(int h1, int h2)
    {
        return Combine(Combine(randomSeed, h1), h2);
    }
 
    private static int CombineHashCodes(int h1, int h2, int h3)
    {
        return Combine(CombineHashCodes(h1, h2), h3);
    }
 
    public override int GetHashCode()
    {
        return CombineHashCodes(
            Item1?.GetHashCode() ?? 0,
            Item2?.GetHashCode() ?? 0,
            Item3?.GetHashCode() ?? 0);
    }
 
    int IStructuralComparable.CompareTo(object obj, IComparer comparer)
    {
        if (obj == null)
        {
            return 1;
        }
 
        if (!(obj is MyValueTuple<T1, T2, T3>))
        {
            throw new ArgumentException("Incorrect type", "obj");
        }
 
        MyValueTuple<T1, T2, T3> other = (MyValueTuple<T1, T2, T3>) obj;
 
        if (Comparer<T1>.Default.Compare(Item1, other.Item1) != 0)
        {
            return Comparer<T1>.Default.Compare(Item1, other.Item1);
        }
 
        if (Comparer<T2>.Default.Compare(Item2, other.Item2) != 0)
        {
            return Comparer<T1>.Default.Compare(Item1, other.Item1);
        }
 
        return Comparer<T3>.Default.Compare(Item3, other.Item3);
    }
 
    bool IStructuralEquatable.Equals(object obj, IEqualityComparer comparer)
    {
        if (!(obj is MyValueTuple<T1, T2, T3>))
        {
            return false;
        }
 
        MyValueTuple<T1, T2, T3> other = (MyValueTuple<T1, T2, T3>) obj;
        return comparer.Equals(
                   Item1,
                   other.Item1)
               && comparer.Equals(
                   Item2,
                   other.Item2)
               && comparer.Equals(
                   Item3,
                   other.Item3);
    }
 
    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer)
    {
        return CombineHashCodes(
            Item1?.GetHashCode() ?? 0,
            Item2?.GetHashCode() ?? 0,
            Item3?.GetHashCode() ?? 0);
    }
 
    int IComparable.CompareTo(object other)
    {
        if (other == null)
        {
            return 1;
        }
 
        if (!(other is MyValueTuple<T1, T2, T3>))
        {
            throw new ArgumentException("Incorrect type", "other");
        }
 
        return CompareTo((MyValueTuple<T1, T2, T3>) other);
    }
 
    public override string ToString()
    {
        return $"({Item1}, {Item2}, {Item3})";
    }
}

Note that the CompareTo and GetHashCode methods are all simply returning 0, but that actually matches the ValueType functionality of CompareTo and GetHashCode. UPDATE: Added implementations of CompareTo and GetHashCode to match the reference source. Also, to fully match ValueType it would be necessary to add similar types for at least up to seven generic parameters.

Now let’s convert our job to use MyValueTuple:

[BurstCompile]
public struct MyValueTupleJob : IJob
{
    public MyValueTuple<int, int, int> Tuple;
    public NativeArray<MyValueTuple<int, int, int>> Array;
 
    public void Execute()
    {
        for (int i = 0; i < Array.Length; ++i)
        {
            Array[i] = Tuple;
        }
    }
}

Here’s how to use it:

MyValueTuple<int, int, int> tuple = new MyValueTuple<int, int, int>(
    1,
    2,
    3);
NativeArray<MyValueTuple<int, int, int>> array
    = new NativeArray<MyValueTuple<int, int, int>>(
        1,
        Allocator.TempJob);
new MyValueTupleJob {Tuple = tuple, Array = array}.Run();
print(array[0]);
array.Dispose();

Neither is quite as clean without the syntactic sugar, but both are still rather readable.

Now let’s take a look at Burst Inspector to see if it was able to compile our job:

    movsxd  rax, dword ptr [rdi + 24]
    test    rax, rax
    jle     .LBB0_3
    mov     ecx, dword ptr [rdi]
    mov     edx, dword ptr [rdi + 4]
    mov     esi, dword ptr [rdi + 8]
    mov     rdi, qword ptr [rdi + 16]
    add     rdi, 8
    .p2align        4, 0x90
.LBB0_2:
    mov     dword ptr [rdi - 8], ecx
    mov     dword ptr [rdi - 4], edx
    mov     dword ptr [rdi], esi
    add     rdi, 12
    dec     rax
    jne     .LBB0_2
.LBB0_3:
    ret

In addition to successfully compiling, we see in the loop body, roughly labeled by LBB0_2, the copy of Item1, Item2, and Item3 out of the tuple and into the NativeArray.

Conclusion

C# tuples and their backing ValueTuple type are not supported by Burst. Fortunately, it’s rather easy to create our own tuple types. Burst will happily and efficiently compile them, even if we lose out on a bit of syntactic sugar.