docs.unity3d.com
    目次を表示する/隠す

    関数ポインター

    データの処理を他のデータの状態に応じて行う動的関数を実装するには、FunctionPointer<T> を使用します。Burst ではデリゲートはマネージオブジェクトとして扱われるので、C# のデリゲート を動的関数で使用することはできません。

    サポートの詳細

    関数ポインターではジェネリックデリゲートはサポートされません。また、他のオープンなジェネリックメソッドの中では BurstCompiler.CompileFunctionPointer<T> をラップしないでください。ラップした場合、Burst では必要な属性へのデリゲートの適用、追加の安全性解析、必要に応じた最適化を行えません。

    引数と戻り値の型には、DllImport および内部呼び出しと同じ制限が適用されます。詳細については、DllImport と内部呼び出し に関するドキュメントを参照してください。

    IL2CPP との相互運用性

    関数ポインターと IL2CPP を相互運用するには、デリゲートに System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute が必要です。呼び出し規約は CallingConvention.Cdecl に設定します。Burst により、BurstCompiler.CompileFunctionPointer<T> と一緒に使用するデリゲートに、この属性が自動的に追加されます。

    関数ポインターの使用

    関数ポインターを使用するには、Burst でコンパイルする静的関数を特定し、以下の作業を行います。

    1. 目的の関数に [BurstCompile] 属性を追加します。
    2. 含まれる型に [BurstCompile] 属性を追加します。これにより、[BurstCompile] 属性が設定された静的メソッドを Burst コンパイラーで検出可能になります。
    3. 目的の関数の "インターフェース" を作成するデリゲートを宣言します。
    4. 関数に [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);
      }
      
    5. 上記の関数ポインターを通常の 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 でエイリアシングを完全に把握できるため、上記コードには他の例よりも広範な変更を加えられます。また、このコードは、関数ポインターを使用した両例よりもかなりシンプルになっています。

    概要
    • サポートの詳細
      • IL2CPP との相互運用性
    • 関数ポインターの使用
      • ジョブでの関数ポインターの使用
      • C# コードでの関数ポインターの使用
    • パフォーマンス上の考慮事項
    トップに戻る
    Copyright © 2023 Unity Technologies — 商標と利用規約
    • 法律関連
    • プライバシーポリシー
    • クッキー
    • 私の個人情報を販売または共有しない
    • Your Privacy Choices (Cookie Settings)