J'ai une boucle écrite en C++ qui est exécutée pour chaque élément d'un tableau de grand nombre entier. À l'intérieur de la boucle, je masque quelques bits de l'entier, puis trouve les valeurs min et max. J'ai entendu dire que si j'utilisais les instructions SSE pour ces opérations, l'exécution serait beaucoup plus rapide que celle d'une boucle normale écrite avec les conditions AND au niveau du bit et les conditions if-else. Ma question est la suivante: dois-je rechercher ces SSE instructions? De plus, que se passe-t-il si mon code est exécuté sur un processeur différent? Cela fonctionnera-t-il encore ou ces instructions sont-elles spécifiques au processeur?
SIMD, dont SSE est un exemple, vous permet d'effectuer la même opération sur plusieurs morceaux de données. Par conséquent, vous n'obtiendrez aucun avantage à utiliser SSE en remplacement direct des opérations sur les entiers. Vous ne pourrez en bénéficier que si vous pouvez effectuer les opérations sur plusieurs éléments de données à la fois. Cela implique de charger certaines valeurs de données contiguës en mémoire, d'effectuer le traitement requis, puis de passer au jeu de valeurs suivant dans le tableau.
Problèmes:
1 Si le chemin du code dépend des données en cours de traitement, SIMD devient beaucoup plus difficile à mettre en œuvre. Par exemple:
a = array [index];
a &= mask;
a >>= shift;
if (a < somevalue)
{
a += 2;
array [index] = a;
}
++index;
n'est pas facile à faire en tant que SIMD:
a1 = array [index] a2 = array [index+1] a3 = array [index+2] a4 = array [index+3]
a1 &= mask a2 &= mask a3 &= mask a4 &= mask
a1 >>= shift a2 >>= shift a3 >>= shift a4 >>= shift
if (a1<somevalue) if (a2<somevalue) if (a3<somevalue) if (a4<somevalue)
// help! can't conditionally perform this on each column, all columns must do the same thing
index += 4
2 Si les données ne sont pas contiguës, le chargement des données dans les instructions SIMD est fastidieux.
3 Le code est spécifique au processeur. SSE est uniquement sur IA32 (Intel/AMD) et tous les processeurs IA32 ne prennent pas en charge SSE.
Vous devez analyser l'algorithme et les données pour voir s'il peut s'agir d'une analyse SSE et cela nécessite de savoir comment fonctionne SSE. Il y a beaucoup de documentation sur le site Web d'Intel.
Ce type de problème est un exemple parfait où un bon profileur de bas niveau est essentiel. (Quelque chose comme VTune) Cela peut vous donner une idée beaucoup plus éclairée de l'endroit où se trouvent vos points chauds.
À mon avis, d'après ce que vous décrivez, votre point chaud sera probablement un échec de la prévision de branche résultant de calculs min/max utilisant if/else. Par conséquent, l’utilisation de SIMD intrinsics devrait vous permettre d’utiliser les instructions min/max. Toutefois, il peut être intéressant d’essayer simplement d’utiliser une calcul min/max sans embranchement. Cela pourrait réaliser la plupart des gains avec moins de douleur.
Quelque chose comme ça:
inline int
minimum(int a, int b)
{
int mask = (a - b) >> 31;
return ((a & mask) | (b & ~mask));
}
Si vous utilisez SSE instructions, vous êtes évidemment limité aux processeurs qui les prennent en charge. Cela signifie x86, remontant au Pentium 2 ou à peu près (je ne me souviens pas exactement à quel moment ils ont été introduits , mais il y a longtemps
SSE2, qui, autant que je puisse me souvenir, est celui qui offre des opérations sur les entiers, est un peu plus récent (Pentium 3? Bien que les premiers processeurs AMD Athlon ne les aient pas supportés)
Dans tous les cas, vous avez deux options pour utiliser ces instructions. Soit écrire tout le bloc de code dans Assembly (ce qui est probablement une mauvaise idée. Il est donc pratiquement impossible pour le compilateur d’optimiser votre code et il est très difficile pour un humain d’écrire un assembleur efficace).
Vous pouvez également utiliser les éléments intrinsèques disponibles avec votre compilateur (si la mémoire sert, ils sont généralement définis dans xmmintrin.h)
Mais encore une fois, la performance ne peut pas améliorer. Le code SSE pose des exigences supplémentaires aux données qu'il traite. Il convient de garder à l’esprit que les données doivent être alignées sur des limites de 128 bits. Il devrait également y avoir peu ou pas de dépendances entre les valeurs chargées dans le même registre (un registre 128 bits SSE peut contenir 4 ints. Ajouter les premier et second ensemble n'est pas optimal. Mais l'addition des quatre ints aux 4 ints correspondants dans un autre registre sera rapide)
Il peut être tentant d’utiliser une bibliothèque qui englobe tous les fiddling SSE de bas niveau, mais cela pourrait également ruiner tout avantage potentiel en termes de performances.
Je ne sais pas à quel point la prise en charge des opérations entières par SSE est bonne, ce qui peut également être un facteur limitant les performances. SSE est principalement destiné à accélérer les opérations en virgule flottante.
Si vous avez l’intention d’utiliser Microsoft Visual C++, lisez ce qui suit:
Je peux dire de mon expérience que SSE apporte une énorme accélération (4 fois et plus) par rapport à une version simplifiée du code (aucun code inline, aucun élément intrinsèque utilisé), mais un assembleur optimisé manuellement peut battre Généré par le compilateur Assemblez si le compilateur ne peut pas comprendre ce que le programmeur avait l'intention de faire (croyez-moi, les compilateurs ne couvrent pas toutes les combinaisons de code possibles et ils ne le feront jamais). Oh, le compilateur ne peut pas à tout moment mettre en forme les données il tourne à la vitesse la plus rapide possible. Mais vous avez besoin de beaucoup d’expérience pour accélérer le processus par rapport à un compilateur Intel (si possible).
Nous avons implémenté du code de traitement d'image, similaire à ce que vous décrivez, mais sur un tableau d'octets, In SSE. L’accélération par rapport au code C est considérable, en fonction de l’algorithme exact plus d’un facteur 4, même en ce qui concerne le compilateur Intel. Cependant, comme vous l'avez déjà mentionné, vous présentez les inconvénients suivants:
Portabilité. Le code fonctionnera sur tous les processeurs de type Intel, donc aussi sur AMD, mais pas sur les autres processeurs. Ce n'est pas un problème pour nous car nous contrôlons le matériel cible. La commutation des compilateurs et même vers un système d’exploitation 64 bits peut également poser problème.
Vous avez une courbe d'apprentissage abrupte, mais j'ai constaté qu'après avoir saisi les principes, écrire de nouveaux algorithmes n'est pas si difficile.
Maintenabilité. La plupart des programmeurs C ou C++ ne connaissent pas Assembly/SSE.
Je vous conseillerai de ne vous lancer que si vous avez vraiment besoin d'amélioration des performances et si vous ne trouvez pas de fonction à votre problème dans une bibliothèque telle que l'intell IPP, et si vous pouvez vous débrouiller avec les problèmes de portabilité.
Ecrivez un code qui aide le compilateur à comprendre ce que vous faites. GCC comprendra et optimisera le code SSE tel que celui-ci:
typedef union Vector4f
{
// Easy constructor, defaulted to black/0 vector
Vector4f(float a = 0, float b = 0, float c = 0, float d = 1.0f):
X(a), Y(b), Z(c), W(d) { }
// Cast operator, for []
inline operator float* ()
{
return (float*)this;
}
// Const ast operator, for const []
inline operator const float* () const
{
return (const float*)this;
}
// ---------------------------------------- //
inline Vector4f operator += (const Vector4f &v)
{
for(int i=0; i<4; ++i)
(*this)[i] += v[i];
return *this;
}
inline Vector4f operator += (float t)
{
for(int i=0; i<4; ++i)
(*this)[i] += t;
return *this;
}
// Vertex / Vector
// Lower case xyzw components
struct {
float x, y, z;
float w;
};
// Upper case XYZW components
struct {
float X, Y, Z;
float W;
};
};
N'oubliez surtout pas d'avoir -msse -msse2 sur vos paramètres de construction!
Les instructions SSE ne concernaient à l'origine que les puces Intel, mais récemment (depuis Athlon?), Elles sont également prises en charge par AMD. Par conséquent, si vous codez avec le jeu d'instructions SSE, vous devez être portable pour la plupart des procs x86.
Cela étant dit, il ne vaut peut-être pas la peine que vous perdiez votre temps à apprendre le codage SSE à moins que vous ne soyez déjà familiarisé avec l'assembleur sur x86 - une option plus simple pourrait être de vérifier la documentation de votre compilateur et de voir s'il existe des options permettant le compilateur à générer automatiquement SSE code pour vous. Certains compilateurs font très bien la vectorisation de boucles de cette façon. (Vous n'êtes probablement pas surpris d'apprendre que les compilateurs d'Intel font un bon travail dans ce domaine :)
Bien qu'il soit vrai que SSE soit spécifique à certains processeurs (SSE peut être relativement sûr, SSE2 beaucoup moins, selon mon expérience), vous pouvez détecter le processeur au moment de l'exécution et charger le code de manière dynamique en fonction du processeur cible. .
Je suis d'accord avec les affiches précédentes. Les avantages peuvent être assez importants, mais pour l'obtenir, cela peut demander beaucoup de travail. La documentation d'Intel sur ces instructions dépasse 4K pages. Vous voudrez peut-être vérifier EasySSE (bibliothèque de wrappers c ++ sur intrinsèques + exemples) sans Ocali Inc.
Je suppose que mon affiliation à cet EasySSE est claire.
Les composants intrinsèques de SIMD (tels que SSE2) peuvent accélérer ce type de processus, mais requièrent une expertise pour être utilisés correctement. Ils sont très sensibles à l'alignement et à la latence du pipeline. une utilisation négligente peut rendre les performances encore pires qu’elles ne l’auraient été. Vous obtiendrez une accélération beaucoup plus facile et plus immédiate en utilisant simplement la prélecture de cache pour vous assurer que toutes vos entrées sont en L1 à temps pour que vous puissiez les exploiter.
À moins que votre fonction ne nécessite un débit supérieur à 100 000 000 entiers par seconde, SIMD n'en vaut probablement pas la peine.
Pour ajouter brièvement à ce qui a été dit précédemment à propos des différentes versions de SSE disponibles sur différents processeurs: Pour vérifier cela, examinez les indicateurs de caractéristiques respectifs renvoyés par l'instruction CPUID (pour plus de détails, reportez-vous à la documentation d'Intel). .
Regardez inline assembler pour C/C++, voici un article DDJ . Sauf si vous êtes certain à 100% que votre programme fonctionnera sur une plate-forme compatible, vous devez suivre les recommandations que beaucoup ont données ici.
Je ne recommande pas de le faire vous-même, à moins que vous ne maîtrisiez assez bien Assembly. L'utilisation de SSE nécessitera plus que probablement une réorganisation minutieuse de vos données, comme le soulignera Skizz , et le bénéfice sera au mieux discutable.
Il serait probablement beaucoup mieux pour vous d'écrire de très petites boucles et de garder vos données très bien organisées et de ne vous fier que sur le compilateur. Le compilateur C d'Intel et GCC (depuis la version 4.1) peuvent vectoriser automatiquement votre code et feront probablement un meilleur travail que vous. (Ajoutez simplement -ftree-vectorize à vos CXXFLAGS.)
Edit : Une autre chose que je devrais mentionner est que plusieurs compilateurs supportent intrinsics de l’assemblage}, qui serait probablement plus facile à utiliser que la syntaxe asm () ou __asm {}.