関数ポインター
データの処理を他のデータの状態に応じて行う動的関数を実装するには、FunctionPointer<T>
を使用します。Burst ではデリゲートはマネージオブジェクトとして扱われるので、C# のデリゲート を動的関数で使用することはできません。
サポートの詳細
関数ポインターではジェネリックデリゲートはサポートされません。また、他のオープンなジェネリックメソッドの中では BurstCompiler.CompileFunctionPointer<T>
をラップしないでください。ラップした場合、Burst では必要な属性へのデリゲートの適用、追加の安全性解析、必要に応じた最適化を行えません。
引数と戻り値の型には、DllImport
および内部呼び出しと同じ制限が適用されます。詳細については、DllImport と内部呼び出し に関するドキュメントを参照してください。
IL2CPP との相互運用性
関数ポインターと IL2CPP を相互運用するには、デリゲートに System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute
が必要です。呼び出し規約は CallingConvention.Cdecl
に設定します。Burst により、BurstCompiler.CompileFunctionPointer<T>
と一緒に使用するデリゲートに、この属性が自動的に追加されます。
関数ポインターの使用
関数ポインターを使用するには、Burst でコンパイルする静的関数を特定し、以下の作業を行います。
- 目的の関数に
[BurstCompile]
属性を追加します。 - 含まれる型に
[BurstCompile]
属性を追加します。これにより、[BurstCompile]
属性が設定された静的メソッドを Burst コンパイラーで検出可能になります。 - 目的の関数の "インターフェース" を作成するデリゲートを宣言します。
関数に
[MonoPInvokeCallbackAttribute]
属性を追加します。この属性を追加する理由は、IL2CPP と目的の関数を連携させるためです。以下に例を示します。// Burst に [BurstCompile] 属性の静的メソッドを探すよう指示します [BurstCompile] class EnclosingType { [BurstCompile] [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] public static float MultiplyFloat(float a, float b) => a * b; [BurstCompile] [MonoPInvokeCallback(typeof(Process2FloatsDelegate))] public static float AddFloat(float a, float b) => a + b; // MultiplyFloat メソッドと AddFloat メソッド用の共通インターフェース public delegate float Process2FloatsDelegate(float a, float b); }
上記の関数ポインターを通常の C# コードからコンパイルします。
// Burst でコンパイルしたバージョンの MultiplyFloat を含めます FunctionPointer<Process2FloatsDelegate> mulFunctionPointer = BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat); // Burst でコンパイルしたバージョンの AddFloat を含めます FunctionPointer<Process2FloatsDelegate> addFunctionPointer = BurstCompiler. CompileFunctionPointer<Process2FloatsDelegate>(AddFloat);
ジョブでの関数ポインターの使用
関数ポインターをジョブで直接使用するには、関数ポインターをジョブ構造体に渡します。
// HPC# ジョブから関数ポインターを呼び出します
var resultMul = mulFunctionPointer.Invoke(1.0f, 2.0f);
var resultAdd = addFunctionPointer.Invoke(1.0f, 2.0f);
Burst のデフォルトでは、ジョブについては関数ポインターを非同期でコンパイルします。関数ポインターの同期コンパイルを強制するには、[BurstCompile(SynchronousCompilation = true)]
を使用します。
C# コードでの関数ポインターの使用
上述の関数ポインターを通常の C# コードで使用するには、パフォーマンスを最大限に引き出せるよう、(デリゲートインスタンスである) FunctionPointer<T>.Invoke
プロパティを静的フィールドにキャッシュします。
private readonly static Process2FloatsDelegate mulFunctionPointerInvoke = BurstCompiler.CompileFunctionPointer<Process2FloatsDelegate>(MultiplyFloat).Invoke;
// C# からデリゲートを呼び出します
var resultMul = mulFunctionPointerInvoke(1.0f, 2.0f);
Burst でコンパイルした関数ポインターを C# で使用すると、関数が P/Invoke
相互運用のオーバーヘッドと比べてとても小さい場合には、純粋な C# バージョンの関数ポインターよりも実行速度が遅くなる可能性があります。
パフォーマンス上の考慮事項
可能であれば、Burst でコンパイルしたコードを実行する際には関数ポインターではなくジョブを使用してください。その理由は、ジョブの方が最適だからです。デフォルトでは、ジョブの安全性システムの方が最適化されているため、Burst でより適切なエイリアシング計算を行えます。
また、[NativeContainer]
構造体 (NativeArray
など) のほとんどは、関数ポインターに直接渡せません。これらを渡すには、ジョブ構造体を使用する必要があります。ネイティブコンテナ構造体に含まれるマネージオブジェクトは、ジョブのコンパイル時に Burst コンパイラーで対処できる安全性チェック用のものであり、関数ポインターに渡すためのものではありません。
以下に、Burst での関数ポインターの不適切な使用例を示します。この例では、関数ポインターは入力ポインターの math.sqrt
を計算して出力ポインターに格納します。MyJob
では、2 つの NativeArray
からこの関数ポインターにソースを供給していますが、このような処理は最適ではありません。
///不適切な関数ポインターの例
[BurstCompile]
public class MyFunctionPointers
{
public unsafe delegate void MyFunctionPointerDelegate(float* input, float* output);
[BurstCompile]
public static unsafe void MyFunctionPointer(float* input, float* output)
{
*output = math.sqrt(*input);
}
}
[BurstCompile]
struct MyJob : IJobParallelFor
{
public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer;
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index)
{
var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr();
var outputPtr = (float*)Output.GetUnsafePtr();
FunctionPointer.Invoke(inputPtr + index, outputPtr + index);
}
}
この例が最適でない理由は以下のとおりです。
- 供給されるのが 1 つのスカラー要素だけであるため、Burst では関数ポインターをベクトル化できません。ベクトル化されないため、パフォーマンスが 4 分の 1 から 8 分の 1 に低下します。
MyJob
では、ネイティブ配列であるInput
とOutput
がエイリアスになる可能性がないことを把握していますが、その情報が関数ポインターに伝えられていません。- メモリ内の他の場所にある関数ポインターに常時分岐するオーバーヘッドが 0 ではありません。
関数ポインターを最適な方法で使用するには、以下のように、関数ポインター内ではデータをバッチ処理するようにします。
[BurstCompile]
public class MyFunctionPointers
{
public unsafe delegate void MyFunctionPointerDelegate(int count, float* input, float* output);
[BurstCompile]
public static unsafe void MyFunctionPointer(int count, float* input, float* output)
{
for (int i = 0; i < count; i++)
{
output[i] = math.sqrt(input[i]);
}
}
}
[BurstCompile]
struct MyJob : IJobParallelForBatch
{
public FunctionPointer<MyFunctionPointers.MyFunctionPointerDelegate> FunctionPointer;
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index, int count)
{
var inputPtr = (float*)Input.GetUnsafeReadOnlyPtr() + index;
var outputPtr = (float*)Output.GetUnsafePtr() + index;
FunctionPointer.Invoke(count, inputPtr, outputPtr);
}
}
変更後の MyFunctionPointer
は、処理対象の要素の count
を受け取り、input
ポインターと output
ポインターをループ処理して多くの計算を行います。また、MyJob
が IJobParallelForBatch
になっており、count
は関数ポインターに直接渡されます。このようにするとパフォーマンスが向上する理由は以下のとおりです。
- Burst によって
MyFunctionPointer
呼び出しがベクトル化されます。 - Burst によって関数ポインターごとに
count
アイテムが処理されるので、関数ポインターを呼び出すオーバーヘッドがcount
回数の分だけ減少します。例えば、128 のバッチを実行する場合、関数ポインターのオーバーヘッドはindex
ごとに変更前の 1/128 になります。 - バッチ処理の結果、バッチ処理をしない場合と比べてパフォーマンスが 1.53 倍になります。
ただし、実現し得る最高のパフォーマンスを実現するには、ジョブを使用してください。ジョブを使用することで、開発者の意図を Burst に最大限伝え、最大限の最適化を実現できます。
[BurstCompile]
struct MyJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float> Input;
[WriteOnly] public NativeArray<float> Output;
public unsafe void Execute(int index)
{
Output[i] = math.sqrt(Input[i]);
}
}
このコードの実行速度は、バッチ処理を利用した関数ポインターの例よりも 1.26 倍速く、バッチ処理を利用していない関数ポインターの例よりも 1.93 倍速くなります。Burst でエイリアシングを完全に把握できるため、上記コードには他の例よりも広範な変更を加えられます。また、このコードは、関数ポインターを使用した両例よりもかなりシンプルになっています。