Tuples in Burst
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<T1, T2, T3>
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 UPDATE: Added implementations of CompareTo
and GetHashCode
methods are all simply returning 0
, but that actually matches the ValueType
functionality of CompareTo and GetHashCode.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.
#1 by L on August 5th, 2019 ·
1) CompareTo and GetHashCode only always return 0 in the case of a non-generic ValueTuple, because it contains no data. proper tuples compute proper values
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/ValueTuple.cs#L576
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/ValueTuple.cs#L607
2) you can regain the syntax by naming your types exactly “System.ValueTuple”
#2 by jackson on August 5th, 2019 ·
1) Good catch! I’ve updated the article with the implementations of
CompareTo
andGetHashCode
.2) Unfortunately, naming the type
System.ValueTuple
results in a ton of compiler warnings like this:#3 by L on August 8th, 2019 ·
yes I am redefining ValueTuple on purpose. the compiler gives priority to the current assembly anyways, so you can just #pragma warning disable it. other assemblies referencing both will have this as an error as it is ambiguous, but you can either make VT internal or disambiguate with an extern alias.
#4 by Dia De Tedio on April 6th, 2020 ·
Será que é possÃvel criar um ValueTuple no namespace System, para fazer com que o compilador não utilize o padrão da bibliotéca .NET na conversão do açucar sintático, então use o nosso?
#5 by jackson on April 6th, 2020 ·
No, see the above comment for the error I got when trying out this idea.
Sorry if I misinterpreted your question as I don’t speak that language so I used Google Translate.
#6 by KornFlaks on January 28th, 2021 ·
Dont need this anymore, unity now supports ValueTuples in burst as of 1.5.0 (pre-release as of this message).
Add support for using ValueTuple types like (int, float) from within Burst code, as long as the types do not enter or escape the Burst function boundaries.
https://docs.unity3d.com/Packages/com.unity.burst@1.5/changelog/CHANGELOG.html