Declaring non-aliasing pointers and structs
Read time 7 minutesLast updated 10 days ago
Use the attribute to give Burst additional information on the aliasing of pointers and structs.
[NoAlias]In most use cases, you won't need to use the attribute. You don't need to apply it to a struct definition that already has a attribute or to fields in job structs because in these cases Burst infers the no-alias information.
[NoAlias][NativeContainer]The attribute is exposed so that you can construct complex data structures where Burst can't infer the aliasing. If you use the attribute on a pointer that could alias with another, it might result in undefined behavior and make it hard to track down bugs.
[NoAlias][NoAlias]You can use this attribute in the following ways:
- On a function parameter it signifies that the parameter doesn't alias with any other parameter to the function.
- On a struct field it signifies that the field doesn't alias with any other field of the struct.
[NoAlias] - On a struct it signifies that the address of the struct can't appear within the struct itself.
- On a function return value it signifies that the returned pointer doesn't alias with any other pointer returned from the same function.
NoAlias function parameter
The following is an example of aliasing:
int Foo(ref int a, ref int b){ b = 13; a = 42; return b;}
For this, Burst produces the following assembly:
mov dword ptr [rdx], 13mov dword ptr [rcx], 42mov eax, dword ptr [rdx]ret
This means that Burst does the following:
- Stores 13 into .
b - Stores 42 into .
a - Reloads the value from to return it.
b
Burst has to reload because it doesn't know whether and are backed by the same memory or not.
babAdd the attribute to the code to change this:
[NoAlias]int Foo([NoAlias] ref int a, ref int b){ b = 13; a = 42; return b;}
For this, Burst produces the following assembly:
mov dword ptr [rdx], 13mov dword ptr [rcx], 42mov eax, 13ret
In this case, the load from has been replaced with moving the constant 13 into the return register.
bNoAlias struct field
The following example is the same as the previous, but applied to a struct:
struct Bar{ public NativeArray<int> a; public NativeArray<float> b;}int Foo(ref Bar bar){ bar.b[0] = 42.0f; bar.a[0] = 13; return (int)bar.b[0];}
For this, Burst produces the following assembly:
mov rax, qword ptr [rcx + 16]mov dword ptr [rax], 1109917696mov rcx, qword ptr [rcx]mov dword ptr [rcx], 13cvttss2si eax, dword ptr [rax]ret
In this case, Burst does the following:
- Loads the address of the data in into
b.rax - Stores 42 into it (is
1109917696, which is0x42280000).42.0f - Loads the address of the data in into
a.rcx - Stores 13 into it.
- Reloads the data in and converts it to an integer for returning.
b
If you know that the two aren't backed by the same memory, you can change the code to the following:
NativeArraysstruct Bar{ [NoAlias] public NativeArray<int> a; [NoAlias] public NativeArray<float> b;}int Foo(ref Bar bar){ bar.b[0] = 42.0f; bar.a[0] = 13; return (int)bar.b[0];}
If you attribute both and with it tells Burst that they don't alias with each other within the struct, which produces the following assembly:
ab[NoAlias]mov rax, qword ptr [rcx + 16]mov dword ptr [rax], 1109917696mov rax, qword ptr [rcx]mov dword ptr [rax], 13mov eax, 42ret
This means that Burst can return the integer constant 42.
NoAlias struct
Burst assumes that the pointer to a struct doesn't appear within the struct itself. However, there are cases where this isn't true:
unsafe struct CircularList{ public CircularList* next; public CircularList() { // The 'empty' list just points to itself. next = this; }}
Lists are one of the few structures where it's normal to have the pointer to the struct accessible from somewhere within the struct itself.
The following example indicates where on a struct can help:
[NoAlias]unsafe struct Bar{ public int i; public void* p;}float Foo(ref Bar bar){ *(int*)bar.p = 42; return ((float*)bar.p)[bar.i];}
This produces the following assembly:
mov rax, qword ptr [rcx + 8]mov dword ptr [rax], 42mov rax, qword ptr [rcx + 8]mov ecx, dword ptr [rcx]movss xmm0, dword ptr [rax + 4*rcx]ret
In this case, Burst:
- Loads into
p.rax - Stores 42 into .
p - Loads into
pagain.rax - Loads into
i.ecx - Returns the index into by
p.i
In this situation, Burst loads twice. This is because it doesn't know if points to the address of the struct . Once it stores 42 into it has to reload the address of from , which is a costly operation.
ppbarppbarAdd to prevent this:
[NoAlias][NoAlias]unsafe struct Bar{ public int i; public void* p;}float Foo(ref Bar bar){ *(int*)bar.p = 42; return ((float*)bar.p)[bar.i];}
This produces the following assembly:
mov rax, qword ptr [rcx + 8]mov dword ptr [rax], 42mov ecx, dword ptr [rcx]movss xmm0, dword ptr [rax + 4*rcx]ret
In this situation, Burst only loads the address of once, because tells it that can't be the pointer to .
p[NoAlias]pbarNoAlias function return
Some functions can only return a unique pointer. For instance, only returns a unique pointer. In this case, gives some useful information to Burst.
malloc[return:NoAlias]The following example uses a bump allocator backed with a stack allocation:
// Only ever returns a unique address into the stackalloc'ed memory.// We've made this no-inline because Burst will always try and inline// small functions like these, which would defeat the purpose of this// example[MethodImpl(MethodImplOptions.NoInlining)]unsafe int* BumpAlloc(int* alloca){ int location = alloca[0]++; return alloca + location;}unsafe int Func(){ int* alloca = stackalloc int[128]; // Store our size at the start of the alloca. alloca[0] = 1; int* ptr1 = BumpAlloc(alloca); int* ptr2 = BumpAlloc(alloca); *ptr1 = 42; *ptr2 = 13; return *ptr1;}
This produces the following assembly:
push rsipush rdipush rbxsub rsp, 544lea rcx, [rsp + 36]movabs rax, offset memsetmov r8d, 508xor edx, edxcall raxmov dword ptr [rsp + 32], 1movabs rbx, offset "BumpAlloc(int* alloca)"lea rsi, [rsp + 32]mov rcx, rsicall rbxmov rdi, raxmov rcx, rsicall rbxmov dword ptr [rdi], 42mov dword ptr [rax], 13mov eax, dword ptr [rdi]add rsp, 544pop rbxpop rdipop rsiret
The key things that Burst does:
- Has in
ptr1.rdi - Has in
ptr2.rax - Stores 42 into .
ptr1 - Stores 13 into .
ptr2 - Loads again to return it.
ptr1
If you add the attribute:
[return: NoAlias][MethodImpl(MethodImplOptions.NoInlining)][return: NoAlias]unsafe int* BumpAlloc(int* alloca){ int location = alloca[0]++; return alloca + location;}unsafe int Func(){ int* alloca = stackalloc int[128]; // Store our size at the start of the alloca. alloca[0] = 1; int* ptr1 = BumpAlloc(alloca); int* ptr2 = BumpAlloc(alloca); *ptr1 = 42; *ptr2 = 13; return *ptr1;}
It produces the following assembly:
push rsipush rdipush rbxsub rsp, 544lea rcx, [rsp + 36]movabs rax, offset memsetmov r8d, 508xor edx, edxcall raxmov dword ptr [rsp + 32], 1movabs rbx, offset "BumpAlloc(int* alloca)"lea rsi, [rsp + 32]mov rcx, rsicall rbxmov rdi, raxmov rcx, rsicall rbxmov dword ptr [rdi], 42mov dword ptr [rax], 13mov eax, 42add rsp, 544pop rbxpop rdipop rsiret
In this case, Burst doesn't reload , and moves 42 into the return register.
ptr2