Share via


ParallelHelper

ParallelHelper には、並列コードを処理するハイ パフォーマンス API が含まれています。 また、特定のデータ セットまたはイテレーションの範囲や領域に対して並列操作をすばやくセットアップして実行するために使用できるパフォーマンス指向のメソッドが含まれています。

Platform API:ParallelHelperIActionIAction2DIRefAction<T>IInAction<T><T>

しくみ

ParallelHelper 型は、次の 3 つの主要な概念に基づいて構築されています。

  • これを使って、ターゲットのイテレーション範囲に対して自動バッチ処理を実行できます。 つまり、使用できる CPU コア数に基づいて、適切な数の作業ユニットが自動的にスケジュールされます。 これは、1 つの並列イテレーションごとに並列コールバックを 1 回呼び出すオーバーヘッドを減らすために行われます。
  • これは C# のジェネリック型の実装方法を多用しており、Action<T> のようなデリゲートではなく特定のインターフェイスを実装する struct 型を使います。 これを行うのは、使われている個々のコールバックの型を JIT コンパイラで "確認" できるようにするためです。その結果、可能であれば、コールバック全体をインライン化できます。 こうすることで、特に非常に小さなコールバックを使う場合は、各並列イテレーションのオーバーヘッドを大幅に減らすことができます。デリゲートの呼び出しだけであれば、わずかなコストしかかかりません。 さらに、コールバックとして struct 型を使うには、クロージャで取り込まれる変数を開発者が手動で処理する必要があります。そうすることで、インスタンス メソッドやその他の値から this ポインターの誤った取り込みを防ぐことができます。このようなことがあると、各コールバック呼び出しが大幅に遅くなる可能性があります。 これは、ImageSharp などの他のパフォーマンス指向のライブラリで使われているものと同じアプローチです。
  • 1D と 2D のループ、副作用のある項目のイテレーション、副作用のない項目のイテレーションという 4 種類のイテレーションを表す 4 種類の API が公開されています。 各アクションの種類には、IActionIAction2DIRefAction<T>IInAction<T><T> という対応する interface 型があり、ParallelHelper API に渡される struct コールバックに適用する必要があります。 開発者はこれを使い、意図がより明確なコードを書くことができます。また、API を使って内部でさらなる最適化を実行できます。

構文

たとえば、ある float[] 配列内のすべての項目を処理し、それぞれに 2 を乗算したい場合があるとします。 この場合、変数を取り込む必要はありません。IRefAction<T>interface を使うだけで、ParallelHelper によって各項目が読み込まれ、自動的にコールバックにフィードされます。 必要なのは、ref float 引数を受け取り、必要な操作を実行するコールバックを定義することだけです。

// Be sure to include this using at the top of the file:
using Microsoft.Toolkit.HighPerformance.Helpers;

// First declare the struct callback
public readonly struct ByTwoMultiplier : IRefAction<float>
{
    public void Invoke(ref float x) => x *= 2;
}

// Create an array and run the callback
float[] array = new float[10000];

ParallelHelper.ForEach<float, ByTwoMultiplier>(array);

ForEach API を使う場合、イテレーション範囲を指定する必要はありません。ParallelHelper によってコレクションがバッチ処理され、各入力項目が自動的に処理されます。 さらに、この特定の例では、struct を引数として渡す必要さえありませんでした。初期化する必要のあるフィールドが含まれていなかったため、ParallelHelper.ForEach を呼び出すときにその型を型引数として指定するだけで済みました。そうすると、その API によってその struct の新しいインスタンスが自動的に作成され、それを使ってさまざまな項目が処理されます。

クロージャの概念を紹介するために、実行時に指定した値を配列要素に乗算する例について説明します。 そのためには、コールバック struct 型でその値を "取り込む" 必要があります。 次のようにして実行できます。

public readonly struct ItemsMultiplier : IRefAction<float>
{
    private readonly float factor;
    
    public ItemsMultiplier(float factor)
    {
        this.factor = factor;
    }

    public void Invoke(ref float x) => x *= this.factor;
}

// ...

ParallelHelper.ForEach(array, new ItemsMultiplier(3.14f));

struct には、定数を使う代わりに、要素の乗算に使う係数を表すフィールドが含まれていることがわかります。 また、ForEach を呼び出すときに、目的の要素を使ってコールバック型のインスタンスを明示的に作成しています。 さらに、このケースでは、C# コンパイラは使っている型引数を自動的に認識できるため、メソッド呼び出しからそれらをまとめて省略できます。

コールバックからアクセスする必要がある値のフィールドを作成するというこのアプローチでは、取り込む値を明示的に宣言できるため、コードの表現力が高まります。 これは、ローカル変数にアクセスするラムダ関数またはローカル関数を宣言するときに、C# コンパイラがバックグラウンドで実行している処理とまったく同じです。

次に別の例を紹介します。今回は For API を使い、配列のすべての項目を並列で初期化します。 今回はターゲット配列を直接取り込んでおり、コールバックに IActioninterface を使っていることに注目してください。これにより、現在の並列イテレーション インデックスが引数としてメソッドに渡されます。

public readonly struct ArrayInitializer : IAction
{
    private readonly int[] array;

    public ArrayInitializer(int[] array)
    {
        this.array = array;
    }

    public void Invoke(int i)
    {
        this.array[i] = i;
    }
}

// ...

ParallelHelper.For(0, array.Length, new ArrayInitializer(array));

Note

コールバック型は struct-s であるため、参照ではなく、並列実行されている各スレッドに "コピー" によって渡されます。 つまり、コールバック型のフィールドとして格納されている値の型もコピーされます。 この詳細を覚えておいてエラーを回避するには、コールバック structreadonly としてマークし、C# コンパイラがそのフィールドの値を変更できないようにすることをお勧めします。 これは、値の型の instance フィールドにのみ適用されます。コールバック struct に任意の型の static フィールドまたは参照フィールドがある場合、その値は並列スレッド間で正しく共有されます。

メソッド

これらは ParallelHelper が公開する 4 つの主要な API であり、IActionIAction2DIRefAction<T>IInAction<T> の各インターフェイスに対応しています。 また、ParallelHelper 型は、これらのメソッドに対する多数のオーバーロードも公開しており、イテレーション範囲や入力コールバックの型を指定するさまざまな方法が用意されています。 ForFor2D は、IAction および IAction2D インスタンス上で機能します。これらを使うのは、基礎となるコレクションに必ずしもマップされない一部の並列作業 (各並列イテレーションのインデックスを使って直接アクセスできるもの) を実行する必要がある場合です。 ForEach のオーバーロードは代わりに IRefAction<T> および IInAction<T> インスタンス上で動作します。これらは、並列イテレーションにより、直接インデックスを作成できるコレクション内の項目に直接マップされる場合に使用できます。 この場合、インデックス作成ロジックも抽象化されるため、各並列呼び出しで考慮する必要があるのは、作業対象の入力項目のみであり、その項目の取得方法ではありません。

その他の例については、単体テストのページを参照してください。