Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Вы можете использовать настойку для максимальной ускорения приложения. Плитка делит потоки на равные прямоугольные подмножества или плитки. Если вы используете соответствующий размер плитки и алгоритм плиток, вы можете получить еще больше ускорения из кода C++ AMP. Основными компонентами тилинга являются:
tile_static
Переменные. Основное преимущество при работе с плиткой — это повышение производительности доступаtile_static
. Доступ к данным в памяти может быть значительно быстрее, чем доступ к данным вtile_static
глобальном пространстве (array
илиarray_view
объектах). Экземпляр переменной создается для каждойtile_static
плитки, а все потоки в плитке имеют доступ к переменной. В типичном алгоритме плитки данные копируются вtile_static
память один раз из глобальной памяти, а затем получают доступ к памяти много раз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
объекта и индекса относительно плитки. Использование локального индекса позволяет упростить чтение и отладку кода. Как правило, для доступаarray
array_view
кtile_static
переменным и глобальным индексированием используется локальная индексация.класс tiled_extent и класс tiled_index. Вместо объекта в вызове
parallel_for_each
используетсяtiled_extent
объектextent
. Вместо объекта в вызовеparallel_for_each
используетсяtiled_index
объектindex
.
Чтобы воспользоваться преимуществами наложения, алгоритм должен секционировать вычислительный домен на плитки, а затем скопировать данные плитки в tile_static
переменные для быстрого доступа.
Пример глобальных, плиток и локальных индексов
Примечание.
Заголовки C++ AMP устарели начиная с Visual Studio 2022 версии 17.0.
Включение всех заголовков AMP приведет к возникновению ошибок сборки. Определите _SILENCE_AMP_DEPRECATION_WARNINGS
перед включением всех заголовков AMP, чтобы замолчать предупреждения.
На следующей схеме представлена матрица 8x9 данных, упорядоченная на 2x3 плитках.
В следующем примере отображаются глобальные, плитки и локальные индексы этой плитки матрицы. Объект 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
.
Вектор
Description
структур копируется в объект 8x9array_view
.Метод
parallel_for_each
вызывается с объектом вtiled_extent
качестве вычислительного домена. Объектtiled_extent
создается путем вызоваextent::tile()
метода переменнойdescriptions
. Параметры типа вызоваextent::tile()
,<2,3>
укажите, что создаются 2x3 плитки. Таким образом, матрица 8x9 состоит из 12 плиток, четырех строк и трех столбцов.Метод
parallel_for_each
вызывается с помощьюtiled_index<2,3>
объекта (t_idx
) в качестве индекса. Параметры типа индекса (t_idx
) должны соответствовать параметрам типа вычислительного домена (descriptions.extent.tile< 2, 3>()
).При выполнении каждого потока индекс
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
метод для вычисления среднего значения каждой плитки. Ниже приведены ключи для понимания примера:
Необработанные данные хранятся в матрице 8x8.
Размер плитки составляет 2x2. Это создает сетку 4x4 плиток, а средние значения можно хранить в матрице 4x4 с помощью
array
объекта. Существует только ограниченное количество типов, которые можно записать по ссылке в функции с ограничением AMP. Классarray
является одним из них.Размер матрицы и размер выборки определяются с помощью
#define
инструкций, так как параметры типа вarray
,extent
array_view
иtiled_index
должны быть константными значениями. Вы также можете использоватьconst int static
объявления. Как дополнительное преимущество, это тривиальное изменение размера выборки, чтобы вычислить среднее значение более 4x4 плиток.Для
tile_static
каждой плитки объявляется массив 2x2 значений с плавающей запятой. Хотя объявление находится в пути кода для каждого потока, создается только один массив для каждой плитки в матрице.Существует строка кода для копирования значений на каждой плитке
tile_static
в массив. Для каждого потока после копирования значения в массив выполнение на поток останавливается из-за вызоваtile_barrier::wait
.Когда все потоки на плитке достигли барьера, можно вычислить среднее значение. Так как код выполняется для каждого потока, существует оператор, который вычисляет
if
среднее значение только в одном потоке. Среднее хранится в средней переменной. Барьер по сути является конструкцией, которая управляет вычислениями по плитке, так как можно использоватьfor
цикл.Данные в переменной
averages
, так как этоarray
объект, должны быть скопированы обратно в узел. В этом примере используется оператор преобразования векторов.В полном примере можно изменить 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
переменные не могут иметь инициализаторы. Вторая проблема заключается в том, что существует состояние гонки для назначения total
, так как все потоки в плитке имеют доступ к переменной в определенном порядке. Вы можете запрограммировать алгоритм, чтобы разрешить доступ только к одному потоку на каждом барьере, как показано далее. Однако это решение не расширяемо.
// 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
доступ к памяти. Объект concurrency::array
выделяет только глобальную память. Может concurrency::array_view
ссылаться на глобальную память, tile_static
память или оба, в зависимости от того, как она была создана. Существует два типа памяти, которые должны быть синхронизированы:
глобальная память
tile_static
Забор памяти гарантирует, что доступ к памяти доступен другим потокам в плитке потока и что доступ к памяти выполняется в соответствии с порядком программы. Чтобы обеспечить это, компиляторы и процессоры не переупорядочение операций чтения и записи через ограждение. В C++ AMP забор памяти создается вызовом одного из следующих методов:
метод tile_barrier::wait: создает ограждение как по всему миру, так и
tile_static
в памяти.метод tile_barrier::wait_with_all_memory_fence: создает ограждение как по всему миру, так и
tile_static
в памяти.метод tile_barrier::wait_with_global_memory_fence: создает ограждение только для глобальной памяти.
метод tile_barrier::wait_with_tile_static_memory_fence: создает ограждение вокруг только
tile_static
памяти.
Вызов определенного забора, который требуется, может повысить производительность приложения. Тип барьера влияет на то, как компилятор и операторы переупорядочения оборудования. Например, если вы используете глобальный забор памяти, он применяется только к глобальным доступам к памяти, поэтому компилятор и оборудование могут переупорядочение 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