私がここで読んだいくつかのコメントから、何らかの理由でStructure of Arrays
(SoA
)over Array of Structures
(AoS
)CUDAのような並列実装の場合?それが本当なら、誰でもその理由を説明できますか?前もって感謝します!
通常、最適なパフォーマンスを得るためのAoSとSoAの選択は、アクセスパターンによって異なります。ただし、これはCUDAに限定されるものではありません-メモリアクセスパターンによってパフォーマンスが大きく影響を受ける可能性があるアーキテクチャにも同様の考慮事項が適用されます。キャッシュがある場所、または連続したメモリアクセスでパフォーマンスが向上している場所(CUDAで合体したメモリアクセスなど).
例えば。 RGBピクセルと個別のRGBプレーンの場合:
struct {
uint8_t r, g, b;
} AoS[N];
struct {
uint8_t r[N];
uint8_t g[N];
uint8_t b[N];
} SoA;
各ピクセルのR/G/Bコンポーネントに同時にアクセスする場合、R、G、Bコンポーネントの連続した読み取りは連続し、通常は同じキャッシュラインに含まれるため、AoSは通常意味があります。 CUDAの場合、これはメモリの読み取り/書き込みの合体も意味します。
ただし、カラープレーンを個別に処理する場合は、SoAが優先される場合があります。すべてのR値を何らかのスケール係数でスケーリングする場合、SoAはすべてのRコンポーネントが連続することを意味します。
さらに考慮すべき点は、パディング/アライメントです。上記のRGBの例では、AoSレイアウトの各要素は3バイトの倍数に揃えられていますが、CUDA、SIMDなどには都合が悪い場合があります。ダミーのuint8_t要素を追加して、4バイトのアライメントを確保してください。ただし、SoAの場合、プレーンはバイトアラインされているため、特定のアルゴリズム/アーキテクチャにとってより便利です。
ほとんどの画像処理タイプのアプリケーションでは、AoSシナリオがはるかに一般的ですが、他のアプリケーション、または特定の画像処理タスクでは、これが常に当てはまるとは限りません。明らかな選択肢がない場合、デフォルトの選択肢としてAoSをお勧めします。
AoS v SoAのより一般的な説明については、 この回答 も参照してください。
Struct of Arrays(SoA)がArray of Structs(AoS)よりも優れたパフォーマンスを示す簡単な例を提供したいだけです。
例では、同じコードの3つの異なるバージョンを検討しています。
特に、バージョン2
は、直線配列の使用を検討します。バージョンのタイミング2
および3
はこの例でも同じであり、結果はバージョン1
。一般的に、読みやすさは犠牲になりますが、たとえば、均一キャッシュからの読み込みはconst __restrict__
この場合。
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include <thrust\device_vector.h>
#include "Utilities.cuh"
#include "TimingGPU.cuh"
#define BLOCKSIZE 1024
/******************************************/
/* CELL STRUCT LEADING TO ARRAY OF STRUCT */
/******************************************/
struct cellAoS {
unsigned int x1;
unsigned int x2;
unsigned int code;
bool done;
};
/*******************************************/
/* CELL STRUCT LEADING TO STRUCT OF ARRAYS */
/*******************************************/
struct cellSoA {
unsigned int *x1;
unsigned int *x2;
unsigned int *code;
bool *done;
};
/*******************************************/
/* KERNEL MANIPULATING THE ARRAY OF STRUCT */
/*******************************************/
__global__ void AoSvsSoA_v1(cellAoS *d_cells, const int N) {
const int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < N) {
cellAoS tempCell = d_cells[tid];
tempCell.x1 = tempCell.x1 + 10;
tempCell.x2 = tempCell.x2 + 10;
d_cells[tid] = tempCell;
}
}
/******************************/
/* KERNEL MANIPULATING ARRAYS */
/******************************/
__global__ void AoSvsSoA_v2(unsigned int * __restrict__ d_x1, unsigned int * __restrict__ d_x2, const int N) {
const int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < N) {
d_x1[tid] = d_x1[tid] + 10;
d_x2[tid] = d_x2[tid] + 10;
}
}
/********************************************/
/* KERNEL MANIPULATING THE STRUCT OF ARRAYS */
/********************************************/
__global__ void AoSvsSoA_v3(cellSoA cell, const int N) {
const int tid = threadIdx.x + blockIdx.x * blockDim.x;
if (tid < N) {
cell.x1[tid] = cell.x1[tid] + 10;
cell.x2[tid] = cell.x2[tid] + 10;
}
}
/********/
/* MAIN */
/********/
int main() {
const int N = 2048 * 2048 * 4;
TimingGPU timerGPU;
thrust::Host_vector<cellAoS> h_cells(N);
thrust::device_vector<cellAoS> d_cells(N);
thrust::Host_vector<unsigned int> h_x1(N);
thrust::Host_vector<unsigned int> h_x2(N);
thrust::device_vector<unsigned int> d_x1(N);
thrust::device_vector<unsigned int> d_x2(N);
for (int k = 0; k < N; k++) {
h_cells[k].x1 = k + 1;
h_cells[k].x2 = k + 2;
h_cells[k].code = k + 3;
h_cells[k].done = true;
h_x1[k] = k + 1;
h_x2[k] = k + 2;
}
d_cells = h_cells;
d_x1 = h_x1;
d_x2 = h_x2;
cellSoA cell;
cell.x1 = thrust::raw_pointer_cast(d_x1.data());
cell.x2 = thrust::raw_pointer_cast(d_x2.data());
cell.code = NULL;
cell.done = NULL;
timerGPU.StartCounter();
AoSvsSoA_v1 << <iDivUp(N, BLOCKSIZE), BLOCKSIZE >> >(thrust::raw_pointer_cast(d_cells.data()), N);
gpuErrchk(cudaPeekAtLastError());
gpuErrchk(cudaDeviceSynchronize());
printf("Timing AoSvsSoA_v1 = %f\n", timerGPU.GetCounter());
//timerGPU.StartCounter();
//AoSvsSoA_v2 << <iDivUp(N, BLOCKSIZE), BLOCKSIZE >> >(thrust::raw_pointer_cast(d_x1.data()), thrust::raw_pointer_cast(d_x2.data()), N);
//gpuErrchk(cudaPeekAtLastError());
//gpuErrchk(cudaDeviceSynchronize());
//printf("Timing AoSvsSoA_v2 = %f\n", timerGPU.GetCounter());
timerGPU.StartCounter();
AoSvsSoA_v3 << <iDivUp(N, BLOCKSIZE), BLOCKSIZE >> >(cell, N);
gpuErrchk(cudaPeekAtLastError());
gpuErrchk(cudaDeviceSynchronize());
printf("Timing AoSvsSoA_v3 = %f\n", timerGPU.GetCounter());
h_cells = d_cells;
h_x1 = d_x1;
h_x2 = d_x2;
// --- Check results
for (int k = 0; k < N; k++) {
if (h_x1[k] != k + 11) {
printf("h_x1[%i] not equal to %i\n", h_x1[k], k + 11);
break;
}
if (h_x2[k] != k + 12) {
printf("h_x2[%i] not equal to %i\n", h_x2[k], k + 12);
break;
}
if (h_cells[k].x1 != k + 11) {
printf("h_cells[%i].x1 not equal to %i\n", h_cells[k].x1, k + 11);
break;
}
if (h_cells[k].x2 != k + 12) {
printf("h_cells[%i].x2 not equal to %i\n", h_cells[k].x2, k + 12);
break;
}
}
}
タイミングは次のとおりです(GTX960で実行された実行)。
Array of struct 9.1ms (v1 kernel)
Struct of arrays 3.3ms (v3 kernel)
Straight arrays 3.2ms (v2 kernel)
SoAはSIMD処理に効果的です。いくつかの理由がありますが、基本的にはレジスタに4つの連続したフロートをロードする方が効率的です。次のようなもので:
float v [4] = {0};
__m128 reg = _mm_load_ps( v );
使用するより:
struct vec { float x; float, y; ....} ;
vec v = {0, 0, 0, 0};
__m128
すべてのメンバーにアクセスすることによるデータ:
__m128 reg = _mm_set_ps(v.x, ....);
配列が16バイトにアライメントされている場合、データのロード/ストアは高速で、一部の操作はメモリ内で直接実行できます。