8.4 KiB
NoAlias attribute
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:
- 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.
- 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], 13
mov dword ptr [rcx], 42
mov eax, dword ptr [rdx]
ret
This means that Burst does the following:
- Stores 13 into
b
. - Stores 42 into
a
. - Reloads the value from
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.
NoAlias 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], 1109917696
mov rcx, qword ptr [rcx]
mov dword ptr [rcx], 13
cvttss2si eax, dword ptr [rax]
ret
In this case, Burst does the following:
- Loads the address of the data in
b
intorax
. - Stores 42 into it (
1109917696
is0x42280000
, which is42.0f
). - Loads the address of the data in
a
intorcx
. - Stores 13 into it.
- Reloads the data in
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.
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 [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:
- Loads
p
intorax
. - Stores 42 into
p
. - Loads
p
intorax
again. - Loads
i
intoecx
. - Returns the index into
p
byi
.
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
.
NoAlias function return
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:
- Has
ptr1
inrdi
. - Has
ptr2
inrax
. - Stores 42 into
ptr1
. - Stores 13 into
ptr2
. - Loads
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.