Use the [NoAlias]
attribute to give Burst additional information on the aliasing of pointers and structs.
In most use cases, you won’t need to use the [NoAlias]
attribute. You don’t need to use it with [NativeContainer]
attributed structs, or with fields in job structs. This is because the Burst compiler infers the no-alias information.
The [NoAlias]
attribute is exposed so that you can construct complex data structures where Burst can’t infer the aliasing. If you use the [NoAlias]
attribute on a pointer that could alias with another, it might result in undefined behavior and make it hard to track down bugs.
You can use this attribute in the following ways:
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], 13
mov dword ptr [rcx], 42
mov eax, dword ptr [rdx]
ret
This means that Burst does the following:
b
.a
.b
to return it.Burst has to reload b
because it doesn’t know whether a
and b
are backed by the same memory or not.
Add the [NoAlias]
attribute to the code to change this:
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], 13
mov dword ptr [rcx], 42
mov eax, 13
ret
In this case, the load from b
has been replaced with moving the constant 13 into the return register.
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], 1109917696
mov rcx, qword ptr [rcx]
mov dword ptr [rcx], 13
cvttss2si eax, dword ptr [rax]
ret
In this case, Burst does the following:
b
into rax
.1109917696
is 0x42280000
, which is 42.0f
).a
into rcx
.b
and converts it to an integer for returning.If you know that the two NativeArrays
aren’t backed by the same memory, you can change the code to the following:
struct 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 a
and b
with [NoAlias]
it tells Burst that they don’t alias with each other within the struct, which produces the following assembly:
mov rax, qword ptr [rcx + 16]
mov dword ptr [rax], 1109917696
mov rax, qword ptr [rcx]
mov dword ptr [rax], 13
mov eax, 42
ret
This means that Burst can return the integer constant 42.
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 [NoAlias]
on a struct can help:
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], 42
mov rax, qword ptr [rcx + 8]
mov ecx, dword ptr [rcx]
movss xmm0, dword ptr [rax + 4*rcx]
ret
In this case, Burst:
p
into rax
.p
.p
into rax
again.i
into ecx
.p
by i
.In this situation, Burst loads p
twice. This is because it doesn’t know if p
points to the address of the struct bar
. Once it stores 42 into p
it has to reload the address of p
from bar
, which is a costly operation.
Add [NoAlias]
to prevent this:
[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], 42
mov ecx, dword ptr [rcx]
movss xmm0, dword ptr [rax + 4*rcx]
ret
In this situation, Burst only loads the address of p
once, because [NoAlias]
tells it that p
can’t be the pointer to bar
.
Some functions can only return a unique pointer. For instance, malloc
only returns a unique pointer. In this case, [return:NoAlias]
gives some useful information to Burst.
[!IMPORTANT] Only use
[return: NoAlias]
on functions that are guaranteed to produce a unique pointer. For example, with bump-allocations, or with things likemalloc
. Burst aggressively inlines functions for performance considerations, so with small functions, Burst inlines them into their parents to produce the same result without the attribute.
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 rsi
push rdi
push rbx
sub rsp, 544
lea rcx, [rsp + 36]
movabs rax, offset memset
mov r8d, 508
xor edx, edx
call rax
mov dword ptr [rsp + 32], 1
movabs rbx, offset "BumpAlloc(int* alloca)"
lea rsi, [rsp + 32]
mov rcx, rsi
call rbx
mov rdi, rax
mov rcx, rsi
call rbx
mov dword ptr [rdi], 42
mov dword ptr [rax], 13
mov eax, dword ptr [rdi]
add rsp, 544
pop rbx
pop rdi
pop rsi
ret
The key things that Burst does:
ptr1
in rdi
.ptr2
in rax
.ptr1
.ptr2
.ptr1
again to return it.If you add the [return: NoAlias]
attribute:
[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 rsi
push rdi
push rbx
sub rsp, 544
lea rcx, [rsp + 36]
movabs rax, offset memset
mov r8d, 508
xor edx, edx
call rax
mov dword ptr [rsp + 32], 1
movabs rbx, offset "BumpAlloc(int* alloca)"
lea rsi, [rsp + 32]
mov rcx, rsi
call rbx
mov rdi, rax
mov rcx, rsi
call rbx
mov dword ptr [rdi], 42
mov dword ptr [rax], 13
mov eax, 42
add rsp, 544
pop rbx
pop rdi
pop rsi
ret
In this case, Burst doesn’t reload ptr2
, and moves 42 into the return register.