utilisation avec pile et Microsoft intrinsics MMX C ++
-
04-10-2019 - |
Question
I ai une boucle d'assembleur en ligne qui ajoute de manière cumulative des éléments d'un tableau de données d'int32 avec des instructions MMX. il utilise, en particulier, le fait que les registres MMX peuvent accueillir 16 INT32 pour calculer 16 différentes sommes cumulées en parallèle.
Je voudrais maintenant convertir ce morceau de code à MMX intrinsics mais je crains que je souffrirai une pénalité de performance, car on ne peut pas explicitement intruct le compilateur d'utiliser les 8 MMX registres à accomulate 16 sommes indépendants.
Quelqu'un peut-il commenter cela et peut-être proposer une solution sur la façon de convertir le morceau de code ci-dessous pour intrinsics d'utilisation?
== assembleur en ligne (seulement partiel à l'intérieur de la boucle) ==
paddd mm0, [esi+edx+8*0] ; add first & second pair of int32 elements
paddd mm1, [esi+edx+8*1] ; add third & fourth pair of int32 elements ...
paddd mm2, [esi+edx+8*2]
paddd mm3, [esi+edx+8*3]
paddd mm4, [esi+edx+8*4]
paddd mm5, [esi+edx+8*5]
paddd mm6, [esi+edx+8*6]
paddd mm7, [esi+edx+8*7] ; add 15th & 16th pair of int32 elements
- points de esi au début du tableau de données
- EDX fournit le décalage dans le réseau de données pour la boucle de courant itération
- le réseau de données est agencé de telle sorte que les éléments pour les 16 montants indépendants sont entrelacées.
La solution
Le VS2010 fait un travail d'optimisation décent sur le code équivalent en utilisant intrinsics. Dans la plupart des cas, il compile la valeur intrinsèque:
sum = _mm_add_pi32(sum, *(__m64 *) &intArray[i + offset]);
dans quelque chose comme:
movq mm0, mmword ptr [eax+8*offset]
paddd mm1, mm0
Ce n'est pas aussi concis que votre padd mm1, [esi+edx+8*offset]
, mais il est sans doute assez proche. Le temps d'exécution est probablement dominée par la recherche en mémoire.
Le hic est que VS semble comme l'ajout de registres MMX seulement à d'autres registres MMX. Le schéma ci-dessus ne fonctionne que pour les 7 premières sommes. La somme 8 exige que certains registre soit sauvegardé temporairement dans la mémoire.
Voici un programme complet et son ensemble compilé correspondant (version build):
#include <stdio.h>
#include <stdlib.h>
#include <xmmintrin.h>
void addWithInterleavedIntrinsics(int *interleaved, int count)
{
// sum up the numbers
__m64 sum0 = _mm_setzero_si64(), sum1 = _mm_setzero_si64(),
sum2 = _mm_setzero_si64(), sum3 = _mm_setzero_si64(),
sum4 = _mm_setzero_si64(), sum5 = _mm_setzero_si64(),
sum6 = _mm_setzero_si64(), sum7 = _mm_setzero_si64();
for (int i = 0; i < 16 * count; i += 16) {
sum0 = _mm_add_pi32(sum0, *(__m64 *) &interleaved[i]);
sum1 = _mm_add_pi32(sum1, *(__m64 *) &interleaved[i + 2]);
sum2 = _mm_add_pi32(sum2, *(__m64 *) &interleaved[i + 4]);
sum3 = _mm_add_pi32(sum3, *(__m64 *) &interleaved[i + 6]);
sum4 = _mm_add_pi32(sum4, *(__m64 *) &interleaved[i + 8]);
sum5 = _mm_add_pi32(sum5, *(__m64 *) &interleaved[i + 10]);
sum6 = _mm_add_pi32(sum6, *(__m64 *) &interleaved[i + 12]);
sum7 = _mm_add_pi32(sum7, *(__m64 *) &interleaved[i + 14]);
}
// reset the MMX/floating-point state
_mm_empty();
// write out the sums; we have to do something with the sums so that
// the optimizer doesn't just decide to avoid computing them.
printf("%.8x %.8x\n", ((int *) &sum0)[0], ((int *) &sum0)[1]);
printf("%.8x %.8x\n", ((int *) &sum1)[0], ((int *) &sum1)[1]);
printf("%.8x %.8x\n", ((int *) &sum2)[0], ((int *) &sum2)[1]);
printf("%.8x %.8x\n", ((int *) &sum3)[0], ((int *) &sum3)[1]);
printf("%.8x %.8x\n", ((int *) &sum4)[0], ((int *) &sum4)[1]);
printf("%.8x %.8x\n", ((int *) &sum5)[0], ((int *) &sum5)[1]);
printf("%.8x %.8x\n", ((int *) &sum6)[0], ((int *) &sum6)[1]);
printf("%.8x %.8x\n", ((int *) &sum7)[0], ((int *) &sum7)[1]);
}
void main()
{
int count = 10000;
int *interleaved = new int[16 * count];
// create some random numbers to add up
// (note that on VS2010, RAND_MAX is just 32767)
for (int i = 0; i < 16 * count; ++i) {
interleaved[i] = rand();
}
addWithInterleavedIntrinsics(interleaved, count);
}
Voici le code assembleur généré pour la partie interne de la boucle de somme (sans son prologue et épilogue). Notez comment la plupart des sommes sont conservées efficacement dans MM1-MM6. Voilà qui contraste avec mm0, qui sert à porter le nombre à ajouter à chaque somme, et MM7, qui sert les deux dernières sommes. La version 7 somme de ce programme ne semble pas avoir problème mm7.
012D1070 movq mm7,mmword ptr [esp+18h]
012D1075 movq mm0,mmword ptr [eax-10h]
012D1079 paddd mm1,mm0
012D107C movq mm0,mmword ptr [eax-8]
012D1080 paddd mm2,mm0
012D1083 movq mm0,mmword ptr [eax]
012D1086 paddd mm3,mm0
012D1089 movq mm0,mmword ptr [eax+8]
012D108D paddd mm4,mm0
012D1090 movq mm0,mmword ptr [eax+10h]
012D1094 paddd mm5,mm0
012D1097 movq mm0,mmword ptr [eax+18h]
012D109B paddd mm6,mm0
012D109E movq mm0,mmword ptr [eax+20h]
012D10A2 paddd mm7,mm0
012D10A5 movq mmword ptr [esp+18h],mm7
012D10AA movq mm0,mmword ptr [esp+10h]
012D10AF movq mm7,mmword ptr [eax+28h]
012D10B3 add eax,40h
012D10B6 dec ecx
012D10B7 paddd mm0,mm7
012D10BA movq mmword ptr [esp+10h],mm0
012D10BF jne main+70h (12D1070h)
Alors, que pouvez-vous faire?
-
Profil les 7-somme et de 8 à somme programmes à base intrinsèque. Choisissez celui qui exécute plus rapidement.
-
Profil de la version qui ajoute qu'un registre MMX à la fois. Il devrait encore être en mesure de tirer profit du fait que les processeurs modernes fetch 64 à 128 octets dans le cache à un moment . Il est pas évident que la version 8 somme serait plus rapide que celui 1 somme. La version 1-somme va chercher le montant exact même de la mémoire, et fait exactement le même nombre d'ajouts MMX. Vous devrez entrelacer les entrées en conséquence cependant.
-
Si votre matériel cible permet, pensez à utiliser instructions SSE . Ceux-ci peuvent ajouter 4 valeurs 32 bits à la fois. SSE est disponible dans CPU Intel depuis le Pentium III en 1999.