次の方法で共有


タイルの使用

タイリングを使用して、アプリの高速化を最大化できます。 タイリングは、スレッドを等しい四角形のサブセットまたはタイルに分割 します。 適切なタイル サイズとタイル アルゴリズムを使用すると、C++ AMP コードからさらに高速化できます。 タイリングの基本的なコンポーネントは次のとおりです。

  • tile_static 変数。 タイリングの主な利点は、 tile_static アクセスによるパフォーマンスの向上です。 tile_static メモリ内のデータへのアクセスは、グローバル空間 (array または array_view オブジェクト) 内のデータにアクセスするよりも大幅に高速になる場合があります。 各タイルについて tile_static 変数のインスタンスが作成され、タイル内のすべてのスレッドがこの変数にアクセスできます。 一般的なタイル アルゴリズムでは、データはグローバル メモリから tile_static メモリに 1 回コピーされた後、 tile_static メモリから何度もアクセスされます。

  • tile_barrier::wait メソッドtile_barrier::waitの呼び出しは、同じタイル内のすべてのスレッドがtile_barrier::waitの呼び出しに達するまで、現在のスレッドの実行を中断します。 スレッドが実行される順序を保証することはできません。タイル内のスレッドが、すべてのスレッドが呼び出しに達するまで、 tile_barrier::wait の呼び出しを超えて実行されることはありません。 つまり、 tile_barrier::wait メソッドを使用すると、スレッド単位ではなくタイル単位でタスクを実行できます。 一般的なタイリング アルゴリズムには、タイル全体の tile_static メモリを初期化し、その後に tile_barrier::waitを呼び出すコードがあります。 tile_barrier::waitに続くコードには、すべてのtile_static値へのアクセスを必要とする計算が含まれています。

  • ローカルインデックスとグローバルインデックス作成。 array_viewまたはarray オブジェクト全体に対するスレッドのインデックスと、タイルに対する相対インデックスにアクセスできます。 ローカル インデックスを使用すると、コードの読み取りとデバッグが容易になります。 通常、ローカル インデックスを使用して tile_static 変数にアクセスし、グローバル インデックスを使用して array 変数と array_view 変数にアクセスします。

  • tiled_extent クラスtiled_index クラスtiled_extent呼び出しでは、extent オブジェクトの代わりにparallel_for_each オブジェクトを使用します。 tiled_index呼び出しでは、index オブジェクトの代わりにparallel_for_each オブジェクトを使用します。

タイル化を利用するには、アルゴリズムでコンピューティング ドメインをタイルにパーティション分割し、タイル データを tile_static 変数にコピーしてアクセスを高速化する必要があります。

グローバル インデックス、タイル インデックス、ローカル インデックスの例

C++ AMP ヘッダーは、Visual Studio 2022 バージョン 17.0 以降では非推奨です。 AMP ヘッダーを含めると、ビルド エラーが発生します。 警告をサイレント状態にするには、AMP ヘッダーを含める前に _SILENCE_AMP_DEPRECATION_WARNINGS を定義します。

次の図は、2x3 タイルに配置されたデータの 8x9 マトリックスを表しています。

8 x 9 のマトリックスを 2 x 3 タイルに分割した図。

次の例では、このタイル マトリックスのグローバル インデックス、タイル インデックス、ローカル インデックスを表示します。 array_view オブジェクトは、Description型の要素を使用して作成されます。 Descriptionは、マトリックス内の要素のグローバル インデックス、タイル インデックス、ローカル インデックスを保持します。 parallel_for_eachの呼び出しのコードは、各要素のグローバル インデックス、タイル インデックス、ローカル インデックスの値を設定します。 出力には、 Description 構造体の値が表示されます。

#include <iostream>
#include <iomanip>
#include <Windows.h>
#include <amp.h>
using namespace concurrency;

const int ROWS = 8;
const int COLS = 9;

// tileRow and tileColumn specify the tile that each thread is in.
// globalRow and globalColumn specify the location of the thread in the array_view.
// localRow and localColumn specify the location of the thread relative to the tile.
struct Description {
    int value;
    int tileRow;
    int tileColumn;
    int globalRow;
    int globalColumn;
    int localRow;
    int localColumn;
};

// A helper function for formatting the output.
void SetConsoleColor(int color) {
    int colorValue = (color == 0)  4 : 2;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colorValue);
}

// A helper function for formatting the output.
void SetConsoleSize(int height, int width) {
    COORD coord;

    coord.X = width;
    coord.Y = height;
    SetConsoleScreenBufferSize(GetStdHandle(STD_OUTPUT_HANDLE), coord);

    SMALL_RECT* rect = new SMALL_RECT();
    rect->Left = 0;
    rect->Top = 0;
    rect->Right = width;
    rect->Bottom = height;
    SetConsoleWindowInfo(GetStdHandle(STD_OUTPUT_HANDLE), true, rect);
}

// This method creates an 8x9 matrix of Description structures.
// In the call to parallel_for_each, the structure is updated
// with tile, global, and local indices.
void TilingDescription() {
    // Create 72 (8x9) Description structures.
    std::vector<Description> descs;
    for (int i = 0; i < ROWS * COLS; i++) {
        Description d = {i, 0, 0, 0, 0, 0, 0};
        descs.push_back(d);
    }

    // Create an array_view from the Description structures.
    extent<2> matrix(ROWS, COLS);
    array_view<Description, 2> descriptions(matrix, descs);

    // Update each Description with the tile, global, and local indices.
    parallel_for_each(descriptions.extent.tile< 2, 3>(),
        [=] (tiled_index< 2, 3> t_idx) restrict(amp)
    {
        descriptions[t_idx].globalRow = t_idx.global[0];
        descriptions[t_idx].globalColumn = t_idx.global[1];
        descriptions[t_idx].tileRow = t_idx.tile[0];
        descriptions[t_idx].tileColumn = t_idx.tile[1];
        descriptions[t_idx].localRow = t_idx.local[0];
        descriptions[t_idx].localColumn= t_idx.local[1];
    });

    // Print out the Description structure for each element in the matrix.
    // Tiles are displayed in red and green to distinguish them from each other.
    SetConsoleSize(100, 150);
    for (int row = 0; row < ROWS; row++) {
        for (int column = 0; column < COLS; column++) {
            SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
            std::cout << "Value: " << std::setw(2) << descriptions(row, column).value << "      ";
        }
        std::cout << "\n";

        for (int column = 0; column < COLS; column++) {
            SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
            std::cout << "Tile:   " << "(" << descriptions(row, column).tileRow << "," << descriptions(row, column).tileColumn << ")  ";
        }
        std::cout << "\n";

        for (int column = 0; column < COLS; column++) {
            SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
            std::cout << "Global: " << "(" << descriptions(row, column).globalRow << "," << descriptions(row, column).globalColumn << ")  ";
        }
        std::cout << "\n";

        for (int column = 0; column < COLS; column++) {
            SetConsoleColor((descriptions(row, column).tileRow + descriptions(row, column).tileColumn) % 2);
            std::cout << "Local:  " << "(" << descriptions(row, column).localRow << "," << descriptions(row, column).localColumn << ")  ";
        }
        std::cout << "\n";
        std::cout << "\n";
    }
}

int main() {
    TilingDescription();
    char wait;
    std::cin >> wait;
}

この例の主な作業は、 array_view オブジェクトの定義と parallel_for_eachの呼び出しにあります。

  1. Description構造体のベクトルは、8x9 array_view オブジェクトにコピーされます。

  2. parallel_for_each メソッドは、tiled_extent オブジェクトをコンピューティング ドメインとして使用して呼び出されます。 tiled_extent オブジェクトは、extent::tile()変数のdescriptions メソッドを呼び出すことによって作成されます。 extent::tile()の呼び出しの型パラメーター<2,3>、2x3 タイルが作成されることを指定します。 したがって、8x9 マトリックスは4行3列の12のタイルに分割されます。

  3. parallel_for_each メソッドは、tiled_index<2,3> オブジェクト (t_idx) をインデックスとして使用して呼び出されます。 インデックス (t_idx) の型パラメーターは、コンピューティング ドメイン (descriptions.extent.tile< 2, 3>()) の型パラメーターと一致する必要があります。

  4. 各スレッドが実行されると、インデックス t_idx は、スレッドが含まれるタイル (tiled_index::tile プロパティ) とタイル内のスレッドの場所 (tiled_index::local プロパティ) に関する情報を返します。

タイル同期—tile_staticとtile_barrier::wait

前の例では、タイルのレイアウトとインデックスを示していますが、それ自体はあまり役に立ちません。 アルゴリズムに不可欠でtile_static変数を利用する場合、タイル処理は特に有効です。 タイル内のすべてのスレッドは tile_static 変数にアクセスできるため、 tile_barrier::wait の呼び出しは tile_static 変数へのアクセスを同期するために使用されます。 タイル内のすべてのスレッドは tile_static 変数にアクセスできますが、タイル内のスレッドの実行順序は保証されません。 次の例では、 tile_static 変数と tile_barrier::wait メソッドを使用して、各タイルの平均値を計算する方法を示します。 この例を理解するための鍵を次に示します。

  1. rawData は 8x8 マトリックスに格納されます。

  2. タイルのサイズは 2 x 2 です。 これにより、タイルの 4 x 4 グリッドが作成されます。平均は、 array オブジェクトを使用して 4x4 マトリックスに格納できます。 AMP 制限付き関数では、参照によってキャプチャできる型の数は限られています。 array クラスはその 1 つです。

  3. #definearrayarray_view、およびextentの型パラメーターは定数値である必要があるため、行列のサイズとサンプル サイズは、tiled_index ステートメントを使用して定義されます。 const int static宣言を使用することもできます。 追加の利点として、サンプル サイズを変更して 4 x 4 タイルの平均を計算するのは簡単です。

  4. 各タイルに対して、 tile_static 2x2 の float 値の配列が宣言されます。 宣言はすべてのスレッドのコード パスにありますが、マトリックス内のタイルごとに 1 つの配列のみが作成されます。

  5. 各タイルの値を tile_static 配列にコピーするコード行があります。 スレッドごとに、値が配列にコピーされた後、 tile_barrier::waitの呼び出しによりスレッドでの実行が停止します。

  6. タイル内のすべてのスレッドがバリアに達すると、平均を計算できます。 コードはすべてのスレッドに対して実行されるため、1 つのスレッドの平均のみを計算する if ステートメントがあります。 平均は averages 変数に格納されます。 バリアは基本的に、 for ループを使用する場合と同様に、タイルによって計算を制御するコンストラクトです。

  7. averages変数内のデータは、array オブジェクトであるため、ホストにコピーし直す必要があります。 この例では、ベクター変換演算子を使用します。

  8. 完全な例では、SAMPLESIZE を 4 に変更すると、コードは他の変更なしで正しく実行されます。

#include <iostream>
#include <amp.h>
using namespace concurrency;

#define SAMPLESIZE 2
#define MATRIXSIZE 8
void SamplingExample() {

    // Create data and array_view for the matrix.
    std::vector<float> rawData;
    for (int i = 0; i < MATRIXSIZE * MATRIXSIZE; i++) {
        rawData.push_back((float)i);
    }
    extent<2> dataExtent(MATRIXSIZE, MATRIXSIZE);
    array_view<float, 2> matrix(dataExtent, rawData);

    // Create the array for the averages.
    // There is one element in the output for each tile in the data.
    std::vector<float> outputData;
    int outputSize = MATRIXSIZE / SAMPLESIZE;
    for (int j = 0; j < outputSize * outputSize; j++) {
        outputData.push_back((float)0);
    }
    extent<2> outputExtent(MATRIXSIZE / SAMPLESIZE, MATRIXSIZE / SAMPLESIZE);
    array<float, 2> averages(outputExtent, outputData.begin(), outputData.end());

    // Use tiles that are SAMPLESIZE x SAMPLESIZE.
    // Find the average of the values in each tile.
    // The only reference-type variable you can pass into the parallel_for_each call
    // is a concurrency::array.
    parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
        [=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
    {
        // Copy the values of the tile into a tile-sized array.
        tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
        tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];

        // Wait for the tile-sized array to load before you calculate the average.
        t_idx.barrier.wait();

        // If you remove the if statement, then the calculation executes for every
        // thread in the tile, and makes the same assignment to averages each time.
        if (t_idx.local[0] == 0 && t_idx.local[1] == 0) {
            for (int trow = 0; trow < SAMPLESIZE; trow++) {
                for (int tcol = 0; tcol < SAMPLESIZE; tcol++) {
                    averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
                }
            }
            averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE * SAMPLESIZE);
        }
    });

    // Print out the results.
    // You cannot access the values in averages directly. You must copy them
    // back to a CPU variable.
    outputData = averages;
    for (int row = 0; row < outputSize; row++) {
        for (int col = 0; col < outputSize; col++) {
            std::cout << outputData[row*outputSize + col] << " ";
        }
        std::cout << "\n";
    }
    // Output for SAMPLESIZE = 2 is:
    //  4.5  6.5  8.5 10.5
    // 20.5 22.5 24.5 26.5
    // 36.5 38.5 40.5 42.5
    // 52.5 54.5 56.5 58.5

    // Output for SAMPLESIZE = 4 is:
    // 13.5 17.5
    // 45.5 49.5
}

int main() {
    SamplingExample();
}

競合状態

次のように、tile_staticという名前のtotal変数を作成し、スレッドごとにその変数をインクリメントしたいと思うかもしれません。

// Do not do this.
tile_static float total;
total += matrix[t_idx];
t_idx.barrier.wait();

averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);

この方法の最初の問題は、変数 tile_static 初期化子を持つことができないことです。 2 つ目の問題は、タイル内のすべてのスレッドが特定の順序で変数にアクセスできないため、 totalへの割り当てに競合状態があることです。 次に示すように、各バリアで 1 つのスレッドのみが合計にアクセスできるようにアルゴリズムをプログラミングできます。 ただし、このソリューションは拡張可能ではありません。

// Do not do this.
tile_static float total;
if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
    total = matrix[t_idx];
}
t_idx.barrier.wait();

if (t_idx.local[0] == 0&& t_idx.local[1] == 1) {
    total += matrix[t_idx];
}
t_idx.barrier.wait();

// etc.

メモリ フェンス

同期する必要があるメモリ アクセスには、グローバル メモリ アクセスと tile_static メモリ アクセスの 2 種類があります。 concurrency::array オブジェクトは、グローバル メモリのみを割り当てます。 concurrency::array_viewは、構築方法に応じて、グローバル メモリ、tile_static メモリ、またはその両方を参照できます。 同期する必要があるメモリには、次の 2 種類があります。

  • グローバル メモリ

  • tile_static

メモリ フェンスを使用すると、スレッド タイル内の他のスレッドがメモリ アクセスを使用でき、メモリ アクセスはプログラムの順序に従って実行されます。 これを保証するために、コンパイラとプロセッサはフェンス全体で読み取りと書き込みの順序を変更しません。 C++ AMP では、次のいずれかのメソッドの呼び出しによってメモリ フェンスが作成されます。

必要な特定のフェンスを呼び出すと、アプリのパフォーマンスが向上します。 バリアの種類は、コンパイラとハードウェアの並べ替えステートメントの方法に影響します。 たとえば、グローバル メモリ フェンスを使用する場合、グローバル メモリ アクセスにのみ適用されるため、コンパイラとハードウェアは、フェンスの両側にある tile_static 変数への読み取りと書き込みを並べ替える可能性があります。

次の例では、バリアはtileValues変数であるtile_staticへの書き込みを同期します。 この例では、tile_barrier::wait_with_tile_static_memory_fenceの代わりにtile_barrier::waitが呼び出されます。

// Using a tile_static memory fence.
parallel_for_each(matrix.extent.tile<SAMPLESIZE, SAMPLESIZE>(),
    [=, &averages] (tiled_index<SAMPLESIZE, SAMPLESIZE> t_idx) restrict(amp)
{
    // Copy the values of the tile into a tile-sized array.
    tile_static float tileValues[SAMPLESIZE][SAMPLESIZE];
    tileValues[t_idx.local[0]][t_idx.local[1]] = matrix[t_idx];

    // Wait for the tile-sized array to load before calculating the average.
    t_idx.barrier.wait_with_tile_static_memory_fence();

    // If you remove the if statement, then the calculation executes
    // for every thread in the tile, and makes the same assignment to
    // averages each time.
    if (t_idx.local[0] == 0&& t_idx.local[1] == 0) {
        for (int trow = 0; trow <SAMPLESIZE; trow++) {
            for (int tcol = 0; tcol <SAMPLESIZE; tcol++) {
                averages(t_idx.tile[0],t_idx.tile[1]) += tileValues[trow][tcol];
            }
        }
    averages(t_idx.tile[0],t_idx.tile[1]) /= (float) (SAMPLESIZE* SAMPLESIZE);
    }
});

こちらも参照ください

C++ AMP (C++ Accelerated Massive Parallelism)
tile_static キーワード