メモリのエイリアシング
メモリのエイリアシングとは、コードによるデータの使用方法を Burst に伝える手段です。これにより、アプリケーションのパフォーマンスを向上させ、最適化できます。
メモリのエイリアシングは、メモリ内の位置が互いに重なった場合に発生します。以下のドキュメントでは、メモリのエイリアシングが発生する場合としない場合の違いについて説明します。
以下の例は、入力配列から出力配列にデータをコピーするジョブを示しています。
[BurstCompile]
private struct CopyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public void Execute()
{
for (int i = 0; i < Input.Length; i++)
{
Output[i] = Input[i];
}
}
}
メモリのエイリアシングが発生しない場合
Input 配列と Output 配列が重なっていない場合、つまりそれぞれのメモリ上の位置が重なっていない場合、コードはこのジョブをサンプルの入出力で実行した後、以下の結果を返します。

エイリアシングなしのメモリ
Burst に NoAlias を指定した場合はスカラーレベルで動作し、先ほどのスカラーループを最適化できます。Burst はベクトル化と呼ばれるプロセスを通じてこれを行います。ベクトル化では、ループを書き換えて要素を小さなバッチとして処理します。例えば、Burst が 4 × 4 の要素のベクターレベルで動作する場合、以下のようになります。

ベクトル化された、エイリアシングなしのメモリ
メモリのエイリアシングが発生する場合
Output 配列が Input 配列と 1 要素ずれて重なっている場合 (例えば、Output[0] が Input[1] を指している場合)、メモリでエイリアシングが発生します。この場合、自動ベクタライザーを使用せずに CopyJob を実行すると、以下のような結果になります。

エイリアシングありのメモリ
Burst がメモリのエイリアシングを認識していない 場合、自動的にループをベクトル化しようとして、以下のような結果になります。

コードのベクトル化が無効な、エイリアシングありのメモリ
このコードの結果は無効であり、Burst で識別できない場合にはバグにつながる可能性があります。
生成されるコード
CopyJob の例には、ループ内に AVX2 をターゲットとする x64 アセンブリが存在します。vmovups 命令は 8 個の浮動小数を移動させるので、単一の自動ベクトル化ループは 4 × 8 個の浮動小数を移動させます。つまり、ループのイテレーションごとに、1 個ではなく 32 個の浮動小数がコピーされることになります。
.LBB0_4:
vmovups ymm0, ymmword ptr [rcx - 96]
vmovups ymm1, ymmword ptr [rcx - 64]
vmovups ymm2, ymmword ptr [rcx - 32]
vmovups ymm3, ymmword ptr [rcx]
vmovups ymmword ptr [rdx - 96], ymm0
vmovups ymmword ptr [rdx - 64], ymm1
vmovups ymmword ptr [rdx - 32], ymm2
vmovups ymmword ptr [rdx], ymm3
sub rdx, -128
sub rcx, -128
add rsi, -32
jne .LBB0_4
test r10d, r10d
je .LBB0_8
以下の例は、Burst のエイリアシングを人為的に無効化して、同じループを Burst でコンパイルした結果を示しています。
.LBB0_2:
mov r8, qword ptr [rcx]
mov rdx, qword ptr [rcx + 16]
cdqe
mov edx, dword ptr [rdx + 4*rax]
mov dword ptr [r8 + 4*rax], edx
inc eax
cmp eax, dword ptr [rcx + 8]
jl .LBB0_2
この結果は完全にスカラーであり、元のエイリアス分析で生成される高度に最適化およびベクトル化されたバリアントに比べ、実行にかかる時間は約 32 倍になります。
関数のクローン作成
Burst がパラメーターと関数との間のエイリアシングを認識している関数呼び出しの場合、Burst はエイリアシングを推論できます。Burst は呼び出された関数にこのことを伝えることで、最適化を向上することができます。
[MethodImpl(MethodImplOptions.NoInlining)]
int Bar(ref int a, ref int b)
{
a = 42;
b = 13;
return a;
}
int Foo()
{
var a = 53;
var b = -2;
return Bar(ref a, ref b);
}
Bar のアセンブリは、以下のようになりそうです。
mov dword ptr [rcx], 42
mov dword ptr [rdx], 13
mov eax, dword ptr [rcx]
ret
このようになる原因は、Burst が Bar 関数内の a と b のエイリアシングを認識していないためです。この結果は、他のコンパイラー技術がこのコードスニペットに対して実行する内容と同じです。
しかし、実際には Burst はもっとスマートです。Burst は関数のクローン作成プロセスで Bar のコピーを作成し、このプロセスにより a と b のエイリアシングプロパティがエイリアスしないことを認識します。そして、Bar への元の呼び出しをコピーへの呼び出しで置き換えます。この結果、以下のようなアセンブリになります。
mov dword ptr [rcx], 42
mov dword ptr [rdx], 13
mov eax, 42
ret
このシナリオでは、Burst は a からの 2 回目のロードを実行しません。
エイリアシングのチェック
エイリアシングは Burst のパフォーマンス最適化機能の要であるため、エイリアシング固有の intrinsic がいくつかあります。
Unity.Burst.CompilerServices.Aliasing.ExpectAliasedは、2 つのポインターがエイリアスになると想定し、そうでない場合はコンパイラーエラーを発生させます。Unity.Burst.CompilerServices.Aliasing.ExpectNotAliasedは、2 つのポインターがエイリアスにならないと想定し、そうでない場合はコンパイラーエラーを発生させます。
例:
using static Unity.Burst.CompilerServices.Aliasing;
[BurstCompile]
private struct CopyJob : IJob
{
[ReadOnly]
public NativeArray<float> Input;
[WriteOnly]
public NativeArray<float> Output;
public unsafe void Execute()
{
// NativeContainer 属性の構造体 (NativeArray など) に対し、ジョブ構造体内で互いのエイリアスになることを禁止します。
ExpectNotAliased(Input.getUnsafePtr(), Output.getUnsafePtr());
// NativeContainer 構造体に対し、他の NativeContainer 構造体内に入ることを禁止します。
ExpectNotAliased(in Input, in Output);
ExpectNotAliased(in Input, Input.getUnsafePtr());
ExpectNotAliased(in Input, Output.getUnsafePtr());
ExpectNotAliased(in Output, Input.getUnsafePtr());
ExpectNotAliased(in Output, Output.getUnsafePtr());
// ただし。自身のエイリアスになることは許可します。
ExpectAliased(in Input, in Input);
ExpectAliased(Input.getUnsafePtr(), Input.getUnsafePtr());
ExpectAliased(in Output, in Output);
ExpectAliased(Output.getUnsafePtr(), Output.getUnsafePtr());
}
}
このようなチェックは、最適化が有効になっている場合にのみ実行されます。その理由は、エイリアシングの適切な推論は、本質的に、インライン展開により関数の内容を把握するオプティマイザーの能力によるからです