web-dev-qa-db-fra.com

Structure des tableaux vs tableau des structures dans CUDA

D'après certains commentaires que j'ai lus ici, pour une raison quelconque, il est préférable d'avoir Structure of Arrays (SoA) sur Array of Structures (AoS) pour les implémentations parallèles comme CUDA? Si c'est vrai, quelqu'un peut-il expliquer pourquoi? Merci d'avance!

41
BugShotGG

Le choix entre AoS et SoA pour des performances optimales dépend généralement du modèle d'accès. Cependant, cela ne se limite pas à CUDA - des considérations similaires s'appliquent à toute architecture où les performances peuvent être considérablement affectées par le modèle d'accès à la mémoire, par exemple où vous avez des caches ou où les performances sont meilleures avec un accès à la mémoire contigu (par exemple, les accès à la mémoire fusionnée dans CUDA).

Par exemple. pour les pixels RVB par rapport aux plans RVB séparés:

struct {
    uint8_t r, g, b;
} AoS[N];

struct {
    uint8_t r[N];
    uint8_t g[N];
    uint8_t b[N];
} SoA;

Si vous allez accéder simultanément aux composants R/G/B de chaque pixel, l'AoS est généralement logique, car les lectures successives des composants R, G, B seront contiguës et généralement contenues dans la même ligne de cache. Pour CUDA, cela signifie également une fusion de la lecture/écriture de la mémoire.

Cependant, si vous envisagez de traiter les plans de couleur séparément, SoA peut être préféré, par exemple si vous souhaitez mettre à l'échelle toutes les valeurs R selon un facteur d'échelle, SoA signifie que tous les composants R seront contigus.

Une autre considération est le remplissage/alignement. Pour l'exemple RVB ci-dessus, chaque élément d'une mise en page AoS est aligné sur un multiple de 3 octets, ce qui peut ne pas être pratique pour CUDA, SIMD, et al - dans certains cas, peut-être même nécessiter un remplissage dans la structure pour rendre l'alignement plus pratique (par exemple ajouter un élément fictif uint8_t pour assurer un alignement de 4 octets). Dans le cas SoA, cependant, les plans sont alignés en octets, ce qui peut être plus pratique pour certains algorithmes/architectures.

Pour la plupart des applications de type traitement d'image, le scénario AoS est beaucoup plus courant, mais pour d'autres applications, ou pour des tâches de traitement d'image spécifiques, ce n'est pas toujours le cas. Lorsqu'il n'y a pas de choix évident, je recommanderais l'AoS comme choix par défaut.

Voir aussi cette réponse pour une discussion plus générale d'AoS contre SoA.

49
Paul R

Je veux juste fournir un exemple simple montrant comment un Struct of Arrays (SoA) fonctionne mieux qu'un Array of Structs (AoS).

Dans l'exemple, je considère trois versions différentes du même code:

  1. SoA (v1)
  2. Tableaux droits (v2)
  3. AoS (v3)

En particulier, la version 2 envisage l'utilisation de tableaux droits. Les horaires des versions 2 et 3 sont les mêmes pour cet exemple et le résultat est meilleur que la version 1. Je soupçonne que, en général, les tableaux droits pourraient être préférables, mais au détriment de la lisibilité, car, par exemple, le chargement à partir du cache uniforme pourrait être activé via const __restrict__ pour ce cas.

#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;
        }
    }

}

Voici les timings (exécutions effectuées sur une GTX960):

Array of struct        9.1ms (v1 kernel)
Struct of arrays       3.3ms (v3 kernel)
Straight arrays        3.2ms (v2 kernel)
3
JackOLantern

SoA est effectivement bon pour le traitement SIMD. Pour plusieurs raisons, mais en gros, il est plus efficace de charger 4 flotteurs consécutifs dans un registre. Avec quelque chose comme:

 float v [4] = {0};
 __m128 reg = _mm_load_ps( v );

que d'utiliser:

 struct vec { float x; float, y; ....} ;
 vec v = {0, 0, 0, 0};

et créez un __m128 données en accédant à tous les membres:

 __m128 reg = _mm_set_ps(v.x, ....);

si vos tableaux sont alignés sur 16 octets, le chargement/stockage des données est plus rapide et certaines opérations peuvent être effectuées directement en mémoire.

1
alexbuisson