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.
Était-ce utile?

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?

  1. Profil les 7-somme et de 8 à somme programmes à base intrinsèque. Choisissez celui qui exécute plus rapidement.

  2. 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.

  3. 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.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top