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]);
en 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é par l'extraction de la mémoire.
Le hic est que VS semble comme l'ajout d'MMX enregistre uniquement à d'autres registres MMX. Le schéma ci-dessus ne fonctionne que pour les 7 premières sommes. La 8ème somme nécessite que certains registres soient enregistrés temporairement en mémoire.
Voici un programme complet et son ensemble compilé (release build) correspondant:
#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 en mm1-mm6. Contraste avec mm0, qui est utilisé pour amener le nombre à ajouter à chaque somme, et avec mm7, qui sert les deux dernières sommes. La version 7-somme de ce programme ne semble pas avoir le 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?
Affichent les programmes basés sur des intrinsèques à somme de somme et à somme de 8. Choisissez celui qui s'exécute plus rapidement.
Profil de la version qui ajoute qu'un registre MMX à la fois. Il devrait encore être en mesure de tirer parti du fait que les processeurs modernes fetch 64 to 128 bytes into the cache at a time. Il n'est pas évident que la version 8-somme serait plus rapide que la version 1-somme. La version 1-sum récupère exactement la même quantité de mémoire, et fait exactement le même nombre d'additions MMX. Vous devrez cependant entrelacer les entrées en conséquence.Si votre matériel cible le permet, utilisez SSE instructions. Ceux-ci peuvent ajouter 4 valeurs de 32 bits à la fois. SSE est disponible dans les processeurs Intel depuis le Pentium III en 1999.