NoAlias 属性
[NoAlias] 属性を使用することで、ポインターおよび構造体のエイリアシングに関する追加情報を Burst に伝えることができます。
ほとんどのユースケースでは、[NoAlias] 属性を使用する必要はありません。[NativeContainer] 属性の構造体や、ジョブ構造体のフィールドでは、この属性を使用する必要はありません。これは、Burst コンパイラーによってエイリアスがないという情報が推論されるためです。
[NoAlias] 属性が公開されている理由は、Burst ではエイリアシングを推論できない複雑なデータ構造体を構築できるようにするためです。別のポインターのエイリアスになる可能性のあるポインターに [NoAlias] 属性を使用すると、未定義の動作が発生し、バグの追跡が困難になる可能性があります。
この属性の使い方は以下のとおりです。
- 関数のパラメーターで、そのパラメーターが関数の他のパラメーターのエイリアスにならないことを示す。
- 構造体のフィールドで、そのフィールドが構造体の他のフィールドのエイリアスにならないことを示す。
- 構造体で、構造体のアドレスがその構造体自体の中に出現しないことを示す。
- 関数の戻り値で、返されるポインターが同じ関数から返される他のポインターのエイリアスにならないことを示す。
NoAlias 関数パラメーター
以下は、エイリアシングの例です。
int Foo(ref int a, ref int b)
{
b = 13;
a = 42;
return b;
}
この場合、Burst によって以下のようなアセンブリが生成されます。
mov dword ptr [rdx], 13
mov dword ptr [rcx], 42
mov eax, dword ptr [rdx]
ret
これは、Burst が以下の処理を実行することを意味します。
- 13 を
bに格納する。 - 42 を
aに格納する。 bの値を再ロードして返す。
Burst が b を再ロードする必要があるのは、a と b が同じメモリに確保されているかどうかを認識できないためです。
以下のように [NoAlias] 属性をコードに追加して変更します。
int Foo([NoAlias] ref int a, ref int b)
{
b = 13;
a = 42;
return b;
}
この場合、Burst によって以下のようなアセンブリが生成されます。
mov dword ptr [rdx], 13
mov dword ptr [rcx], 42
mov eax, 13
ret
今度は、b のロードが、リターンレジスタに定数 13 を移動する処理で置き換えられています。
NoAlias 構造体フィールド
以下の例は前の例と同じものですが、適用対象が構造体になっています。
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];
}
この場合、Burst によって以下のようなアセンブリが生成されます。
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
この場合、Burst は以下を実行します。
bのデータのアドレスをraxにロードする。- 42 をこれにを格納する (
1109917696は0x42280000なので42.0fとなる)。 aのデータのアドレスをrcxにロードする。- 13 をこれに格納する。
bのデータを再ロードして、返す準備として整数に変換する。
2 つの NativeArrays が同じメモリに確保されないことがわかっている場合は、コードを以下のように変更できます。
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];
}
a と b の両方に [NoAlias] 属性を設定すると、Burst はこの 2 つが構造体内で互いのエイリアスにならないものと認識し、以下のようなアセンブリを生成します。
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
つまり、Burst からは整数の定数 42 が返されます。
NoAlias 構造体
Burst では、構造体へのポインターが構造体自体の中に出現しないことを前提にしています。ただし、以下のようにこの前提が成立しない場合もあります。
unsafe struct CircularList
{
public CircularList* next;
public CircularList()
{
// "空の" リストはそれ自体を指します。
next = this;
}
}
リストは、通常ではこの構造体へのポインターに対して構造体自体の内部のどこからでもアクセス可能である、数少ない構造体の 1 つです。
以下に、構造体に [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];
}
これにより、以下のアセンブリが生成されます。
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
この場合、Burst は以下を実行します。
pをraxにロードする。- 42 を
pに格納する。 pをraxに再ロードする。iをecxにロードする。pにあるインデックスのi番目の要素を返す。
この場合、Burst は p を 2 回ロードします。これは、p が構造体 bar のアドレスを指しているかどうかが Burst にはわからないためです。一度 p に 42 を格納してから、bar から p のアドレスを再ロードする必要があるため、処理には大きなコストがかかります。
これを回避するために [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];
}
これにより、以下のアセンブリが生成されます。
mov rax, qword ptr [rcx + 8]
mov dword ptr [rax], 42
mov ecx, dword ptr [rcx]
movss xmm0, dword ptr [rax + 4*rcx]
ret
こうすると、p が bar へのポインターになりえないことが [NoAlias] により明示されるので、Burst が p のアドレスをロードする回数は 1 回だけになります。
NoAlias 関数戻り値
関数によっては、一意のポインターしか返せないものがあります。例えば、malloc は一意のポインターしか返しません。この場合、[return:NoAlias] を使うと Burst に有益な情報を提供できます。
Important
[return: NoAlias] は、一意のポインターを生成することが保証されている関数でのみ使用してください。例えば、バンプアロケーションや、malloc などの関数です。Burst はパフォーマンスを考慮し、積極的に関数をインライン化します。そのため、小さな関数は、属性なしでも結果が変わらないようにその親にインライン化されます。
以下の例では、スタックアロケーションでメモリを確保したバンプアロケーターを使用しています。
// スタック領域が割り当てられたメモリに一意のアドレスを返すだけです。
// Burst はこのような小さな関数を常にインライン化しようとするため、
// この例の目的を達することができないので、
// この関数は非インラインにしています
[MethodImpl(MethodImplOptions.NoInlining)]
unsafe int* BumpAlloc(int* alloca)
{
int location = alloca[0]++;
return alloca + location;
}
unsafe int Func()
{
int* alloca = stackalloc int[128];
// alloca の開始時にサイズを格納します。
alloca[0] = 1;
int* ptr1 = BumpAlloc(alloca);
int* ptr2 = BumpAlloc(alloca);
*ptr1 = 42;
*ptr2 = 13;
return *ptr1;
}
これにより、以下のアセンブリが生成されます。
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
Burst が実行する主な処理は以下のとおりです。
ptr1をrdiに保持する。ptr2をraxに保持する。- 42 を
ptr1に格納する。 - 13 を
ptr2に格納する。 ptr1を再ロードして返す。
[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];
// alloca の開始時にサイズを格納します。
alloca[0] = 1;
int* ptr1 = BumpAlloc(alloca);
int* ptr2 = BumpAlloc(alloca);
*ptr1 = 42;
*ptr2 = 13;
return *ptr1;
}
これにより、以下のアセンブリが生成されます。
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
この場合、Burst は ptr2 を再ロードせず、42 をリターンレジスタに移動します。