État d'esprit orienté données
Une conception orientée données ne signifie pas appliquer des SoAs partout. Cela signifie simplement concevoir des architectures avec un accent prédominant sur la représentation des données - en particulier avec un accent sur une disposition et un accès mémoire efficaces.
Cela pourrait éventuellement conduire à des représentants SoA le cas échéant, comme suit:
struct BallSoa
{
vector<float> x; // size n
vector<float> y; // size n
vector<float> z; // size n
vector<float> r; // size n
};
... cela convient souvent à la logique en boucle verticale qui ne traite pas simultanément les composantes du vecteur central d'une sphère et le rayon (les quatre champs ne sont pas simultanément chauds), mais à la place un par un (une boucle à travers le rayon, 3 autres boucles à travers les composants individuels des centres de sphères).
Dans d'autres cas, il pourrait être plus approprié d'utiliser un AoS si les champs sont fréquemment accédés ensemble (si votre logique en boucle parcourt tous les champs de balles plutôt qu'individuellement) et / ou si l'accès aléatoire d'une balle est nécessaire:
struct BallAoS
{
float x;
float y;
float z;
float r;
};
vector<BallAoS> balls; // size n
... dans d'autres cas, il pourrait être approprié d'utiliser un hybride qui équilibre les deux avantages:
struct BallAoSoA
{
float x[8];
float y[8];
float z[8];
float r[8];
};
vector<BallAoSoA> balls; // size n/8
... vous pourriez même compresser la taille d'une balle à moitié en utilisant des demi-flottants pour ajuster plus de champs de balle dans une ligne / page de cache.
struct BallAoSoA16
{
Float16 x2[16];
Float16 y2[16];
Float16 z2[16];
Float16 r2[16];
};
vector<BallAoSoA16> balls; // size n/16
... peut-être même que le rayon n'est pas accessible presque aussi souvent que le centre de la sphère (peut-être que votre base de code les traite souvent comme des points et rarement comme des sphères, par exemple). Dans ce cas, vous pouvez appliquer davantage une technique de division de champ chaud / froid.
struct BallAoSoA16Hot
{
Float16 x2[16];
Float16 y2[16];
Float16 z2[16];
};
vector<BallAoSoA16Hot> balls; // size n/16: hot fields
vector<Float16> ball_radiuses; // size n: cold fields
La clé d'une conception orientée données est de prendre en compte tous ces types de représentations dès le début de vos décisions de conception, pour ne pas vous piéger dans une représentation sous-optimale avec une interface publique derrière.
Il met en lumière les modèles d'accès à la mémoire et les dispositions d'accompagnement, ce qui en fait une préoccupation beaucoup plus forte que d'habitude. Dans un sens, il peut même quelque peu abattre les abstractions. J'ai trouvé en appliquant cet état d'esprit plus que je ne regarde plus std::deque
, par exemple, en termes de ses exigences algorithmiques autant que la représentation agrégée de blocs contigus qu'il a et comment l'accès aléatoire de celui-ci fonctionne au niveau de la mémoire. Il met un peu l'accent sur les détails d'implémentation, mais les détails d'implémentation qui ont tendance à avoir autant ou plus d'impact sur les performances que la complexité algorithmique décrivant l'évolutivité.
Optimisation prématurée
Une grande partie de l'objectif prédominant de la conception orientée données apparaîtra, du moins en un coup d'œil, comme dangereusement proche d'une optimisation prématurée. L'expérience nous enseigne souvent que de telles micro-optimisations sont mieux appliquées avec le recul et avec un profileur en main.
Pourtant, un message fort à tirer de la conception orientée données est peut-être de laisser de la place à de telles optimisations. C'est ce qu'un état d'esprit orienté données peut permettre:
La conception orientée données peut laisser une marge de manœuvre pour explorer des représentations plus efficaces. Il ne s'agit pas nécessairement d'atteindre la perfection de la disposition de la mémoire d'un seul coup, mais plutôt de faire les considérations appropriées à l'avance pour permettre des représentations de plus en plus optimales.
Conception granulaire orientée objet
De nombreuses discussions sur la conception orientée données s'opposeront aux notions classiques de programmation orientée objet. Pourtant, je proposerais une façon de voir les choses qui n'est pas aussi hardcore que de rejeter complètement la POO.
La difficulté avec la conception orientée objet est qu'elle nous tentera souvent de modéliser des interfaces à un niveau très granulaire, nous laissant piégés avec un état d'esprit scalaire, un à la fois au lieu d'un état d'esprit en vrac parallèle.
À titre d'exemple exagéré, imaginez un état d'esprit de conception orienté objet appliqué à un seul pixel d'une image.
class Pixel
{
public:
// Pixel operations to blend, multiply, add, blur, etc.
private:
Image* image; // back pointer to access adjacent pixels
unsigned char rgba[4];
};
Espérons que personne ne le fasse réellement. Pour rendre l'exemple vraiment grossier, j'ai stocké un pointeur arrière sur l'image contenant le pixel afin qu'il puisse accéder aux pixels voisins pour les algorithmes de traitement d'image comme le flou.
Le pointeur de retour d'image ajoute immédiatement un surcoût flagrant, mais même si nous l'excluons (en ne faisant que l'interface publique du pixel fournir des opérations qui s'appliquent à un seul pixel), nous nous retrouvons avec une classe juste pour représenter un pixel.
Maintenant, il n'y a rien de mal avec une classe dans le sens de surcharge immédiate dans un contexte C ++ à part ce pointeur arrière. L'optimisation des compilateurs C ++ est excellente pour prendre toute la structure que nous avons construite et l'effacer vers le bas.
La difficulté ici est que nous modélisons une interface encapsulée à un niveau trop granulaire d'un pixel. Cela nous laisse piégés par ce type de conception et de données granulaires, avec potentiellement un grand nombre de dépendances client les couplant à cette Pixel
interface.
Solution: effacez la structure orientée objet d'un pixel granulaire et commencez à modéliser vos interfaces à un niveau plus grossier traitant un nombre important de pixels (au niveau de l'image).
En modélisant au niveau de l'image en vrac, nous avons beaucoup plus d'espace à optimiser. Nous pouvons, par exemple, représenter de grandes images comme des tuiles coalescentes de 16x16 pixels qui s'intègrent parfaitement dans une ligne de cache de 64 octets mais permettent un accès vertical voisin efficace de pixels avec une petite foulée typiquement (si nous avons un certain nombre d'algorithmes de traitement d'image qui besoin d'accéder aux pixels voisins de façon verticale) comme exemple hardcore orienté données.
Concevoir à un niveau plus grossier
L'exemple ci-dessus de modélisation des interfaces au niveau de l'image est une sorte d'exemple évident car le traitement d'image est un domaine très mature qui a été étudié et optimisé à mort. Pourtant, moins évident pourrait être une particule dans un émetteur de particules, un sprite contre une collection de sprites, un bord dans un graphique de bords, ou même une personne contre une collection de personnes.
La clé pour permettre des optimisations axées sur les données (de manière prospective ou rétrospective) va souvent se résumer à la conception d'interfaces à un niveau beaucoup plus grossier, en vrac. L'idée de concevoir des interfaces pour des entités uniques est remplacée par la conception de collections d'entités avec de grandes opérations qui les traitent en vrac. Cela cible particulièrement et immédiatement les boucles d'accès séquentielles qui ont besoin d'accéder à tout et ne peuvent s'empêcher d'avoir une complexité linéaire.
La conception orientée données commence souvent par l'idée de fusionner les données pour former des agrégats modélisant les données en masse. Un état d'esprit similaire fait écho aux conceptions d'interface qui l'accompagnent.
C'est la leçon la plus précieuse que j'ai tirée de la conception orientée données, car je ne suis pas assez averti en architecture informatique pour trouver souvent la disposition de mémoire la plus optimale pour quelque chose lors de mon premier essai. Cela devient quelque chose que je répète avec un profileur à la main (et parfois avec quelques ratés en cours de route où je n'ai pas réussi à accélérer les choses). Pourtant, l'aspect de conception d'interface de la conception orientée données est ce qui me laisse la place pour rechercher des représentations de données de plus en plus efficaces.
La clé est de concevoir des interfaces à un niveau plus grossier que ce que nous sommes généralement tentés de faire. Cela a également souvent des avantages secondaires comme l'atténuation de la surcharge de répartition dynamique associée aux fonctions virtuelles, les appels de pointeur de fonction, les appels dylib et l'impossibilité pour ceux d'être alignés. L'idée principale à retirer de tout cela est d'examiner le traitement en bloc (le cas échéant).
ball->do_something();
contreball_table.do_something(ball)
) à moins que vous ne vouliez simuler une entité cohérente via un pseudo-pointeur(&ball_table, index)
.