2010-01-05 4 views
2

J'effectue une lecture dispersée de données de 8 bits à partir d'un fichier (De-Interleaving un fichier d'onde de 64 canaux). Je les combine ensuite pour former un seul flux d'octets. Le problème que j'ai est avec ma reconstruction des données à écrire. Fondamentalement, je lis 16 octets et ensuite les construire en une seule variable __m128i, puis en utilisant _mm_stream_ps pour écrire la valeur en mémoire. Cependant, j'ai des résultats de performance bizarres.Bizarre d'optimisation intrinsèque VC++ SSE

Dans mon premier schéma j'utilise la _mm_set_epi8 intrinsèque pour définir mon __m128i comme suit:

const __m128i packedSamples = _mm_set_epi8(sample15, sample14, sample13, sample12, sample11, sample10, sample9, sample8, 
               sample7, sample6, sample5, sample4, sample3, sample2, sample1, sample0); 

Fondamentalement, je laisse le tout au compilateur de décider comment l'optimiser pour obtenir la meilleure performance. Cela donne la pire performance. MON test s'exécute en ~ 0.195 secondes.

Deuxièmement, j'ai essayé de fusionner vers le bas en utilisant 4 _mm_set_epi32 instructions, puis les tassant:

const __m128i samples0  = _mm_set_epi32(sample3, sample2, sample1, sample0); 
    const __m128i samples1  = _mm_set_epi32(sample7, sample6, sample5, sample4); 
    const __m128i samples2  = _mm_set_epi32(sample11, sample10, sample9, sample8); 
    const __m128i samples3  = _mm_set_epi32(sample15, sample14, sample13, sample12); 

    const __m128i packedSamples0 = _mm_packs_epi32(samples0, samples1); 
    const __m128i packedSamples1 = _mm_packs_epi32(samples2, samples3); 
    const __m128i packedSamples  = _mm_packus_epi16(packedSamples0, packedSamples1); 

Cela n'améliore quelque peu les performances. Mon test fonctionne maintenant dans ~ 0.15 secondes. Il semble contre-intuitif que les performances s'améliorent en faisant cela car je suppose que c'est exactement ce que _mm_set_epi8 fait de toute façon ...

Ma dernière tentative a été d'utiliser un peu de code que j'ai pour faire quatre CCs à l'ancienne (avec des décalages et des orcs) et ensuite les mettre dans un __m128i en utilisant un seul _mm_set_epi32.

const GCui32 samples0  = MakeFourCC(sample0, sample1, sample2, sample3); 
    const GCui32 samples1  = MakeFourCC(sample4, sample5, sample6, sample7); 
    const GCui32 samples2  = MakeFourCC(sample8, sample9, sample10, sample11); 
    const GCui32 samples3  = MakeFourCC(sample12, sample13, sample14, sample15); 
    const __m128i packedSamples = _mm_set_epi32(samples3, samples2, samples1, samples0); 

Cela donne des résultats encore meilleurs. Prendre ~ 0.135 secondes pour exécuter mon test. Je commence vraiment à être confus. J'ai donc essayé un simple octet de lecture octet octet système et c'est toujours un peu plus rapide que la dernière méthode.

Alors, que se passe-t-il? Tout cela me semble contre-intuitif. J'ai considéré l'idée que les retards se produisaient sur le _mm_stream_ps parce que je fournissais des données trop rapidement mais alors j'obtiendrais exactement les mêmes résultats dehors que je fais. Est-il possible que les 2 premières méthodes signifient que les 16 charges ne peuvent pas être distribuées à travers la boucle pour cacher la latence? Si oui, pourquoi est-ce? Sûrement un intrinsèque permet au compilateur de faire des optimisations au fur et à mesure qu'il le souhaite .. je pensais que c'était tout le point ... Aussi sûrement effectuer 16 lectures et 16 écritures sera beaucoup plus lent que 16 lectures et 1 écrire avec un tas de jonglerie SSE instructions ... Après tout ce sont les lectures et les écritures qui sont le bit lent!

Toute personne ayant des idées ce qui se passe sera très appréciée! : D

Edit: Suite au commentaire ci-dessous je me suis arrêté avant le chargement des octets comme des constantes et changedit à ceci:

const __m128i samples0  = _mm_set_epi32(*(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0)); 
    pSamples += channelStep4; 
    const __m128i samples1  = _mm_set_epi32(*(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0)); 
    pSamples += channelStep4; 
    const __m128i samples2  = _mm_set_epi32(*(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0)); 
    pSamples += channelStep4; 
    const __m128i samples3  = _mm_set_epi32(*(pSamples + channelStep3), *(pSamples + channelStep2), *(pSamples + channelStep1), *(pSamples + channelStep0)); 
    pSamples += channelStep4; 

    const __m128i packedSamples0 = _mm_packs_epi32(samples0, samples1); 
    const __m128i packedSamples1 = _mm_packs_epi32(samples2, samples3); 
    const __m128i packedSamples  = _mm_packus_epi16(packedSamples0, packedSamples1); 

et cette amélioration de la performance à ~ 0,143 secondes. Tujoruos pas aussi bon que la mise en œuvre droite C ...

Modifier Encore une fois: La meilleure performance que je reçois est donc loin

// Load the samples. 
    const GCui8 sample0  = *(pSamples + channelStep0); 
    const GCui8 sample1  = *(pSamples + channelStep1); 
    const GCui8 sample2  = *(pSamples + channelStep2); 
    const GCui8 sample3  = *(pSamples + channelStep3); 

    const GCui32 samples0 = Build32(sample0, sample1, sample2, sample3); 
    pSamples += channelStep4; 

    const GCui8 sample4  = *(pSamples + channelStep0); 
    const GCui8 sample5  = *(pSamples + channelStep1); 
    const GCui8 sample6  = *(pSamples + channelStep2); 
    const GCui8 sample7  = *(pSamples + channelStep3); 

    const GCui32 samples1 = Build32(sample4, sample5, sample6, sample7); 
    pSamples += channelStep4; 

    // Load the samples. 
    const GCui8 sample8  = *(pSamples + channelStep0); 
    const GCui8 sample9  = *(pSamples + channelStep1); 
    const GCui8 sample10 = *(pSamples + channelStep2); 
    const GCui8 sample11 = *(pSamples + channelStep3); 

    const GCui32 samples2  = Build32(sample8, sample9, sample10, sample11); 
    pSamples += channelStep4; 

    const GCui8 sample12 = *(pSamples + channelStep0); 
    const GCui8 sample13 = *(pSamples + channelStep1); 
    const GCui8 sample14 = *(pSamples + channelStep2); 
    const GCui8 sample15 = *(pSamples + channelStep3); 

    const GCui32 samples3 = Build32(sample12, sample13, sample14, sample15); 
    pSamples += channelStep4; 

    const __m128i packedSamples = _mm_set_epi32(samples3, samples2, samples1, samples0); 

    _mm_stream_ps(pWrite + 0, *(__m128*)&packedSamples); 

Cela me donne le traitement en ~ 0,095 secondes, ce qui est beaucoup mieux. Je ne semble pas être en mesure de se rapprocher avec SSE si ... Je suis toujours confus par cela, mais ..ho hum.

+1

Jetez un oeil à l'assembleur produit par le compilateur. Peut-être que cela donne un aperçu du problème. –

+0

Hmm fair point ... Je devrais vérifier que ... le compilateur génère du code VRAIMENT bizarre ... Si seulement il y avait un intrinsèque pour les lectures dispersées :( – Goz

+0

Vous avez doublé les performances par rapport à l'implémentation intrinsèque naïve! mauvais et vous utilisez SSE, pour assembler les morceaux de 4 octets en 16 octets et enfin le stocker.Après le déversement, le problème est que vous n'avez pas beaucoup de travail pour SSE à faire, et vous devez éviter de trop bouger inss des registres généraux aux registres SSE SSE brillerait vraiment si vous lisiez un flux continu, mais ce n'est pas le cas – Potatoswatter

Répondre

2

Peut-être que le compilateur tente de placer tous les arguments de l'intrinsèque dans des registres à la fois. Vous ne voulez pas accéder à autant de variables à la fois sans les organiser. Plutôt que de déclarer un identificateur distinct pour chaque échantillon, essayez de les placer dans un char[16]. Le compilateur va promouvoir les 16 valeurs aux registres comme bon lui semble, tant que vous ne prenez pas l'adresse de quoi que ce soit dans le tableau. Vous pouvez ajouter une balise __aligned__ (ou toute autre utilisation de VC++) et peut-être éviter l'intrinsèque. Sinon, appeler l'intrinsèque avec (sample[15], sample[14], sample[13] … sample[0]) devrait faciliter le travail du compilateur ou au moins ne pas nuire.


Edit: Je suis sûr que vous vous battez contre un déversement de registre, mais cette suggestion sera probablement stocker les octets individuellement, ce qui est pas ce que vous voulez. Je pense que mon conseil est d'entrelacer votre dernière tentative (en utilisant MakeFourCC) avec les opérations de lecture, pour s'assurer que c'est planifié correctement et sans aller-retour à la pile. Bien sûr, l'inspection du code objet est le meilleur moyen d'assurer cela. Essentiellement, vous diffusez des données dans le fichier de registre, puis vous le retransmettez. Vous ne voulez pas le surcharger avant qu'il soit temps de vider les données.

+0

chose est en faisant cela, je peux aussi bien les écrire tous à la mémoire. Cela me donne des idées ... Je commence à penser que je pourrais obtenir de meilleures performances en écrivant un simple assembleur. Je voulais juste éviter un bloc assembleur pour des raisons 64 bits ... J'espérais vraiment que le compilateur s'en occuperait pour moi ... mon erreur;) – Goz

+0

C'est pourquoi j'ai fait le montage ... le vrai point clé est d'assurer que les octets sont assemblés à leur arrivée. Ensuite, vous avez au plus trois variables de 4 octets et deux variables de 2 octets (car x86 peut déjà adresser des octets hauts/bas) pour cinq registres maximum avant d'appeler '_mm_set_epi32'. – Potatoswatter

+0

J'ai juste essayé exactement ce que vous dites dans votre édition. Soudain, le temps d'exécution est réduit à ~ 0,095 secondes. Je pensais que le compilateur effectuerait ce genre de ré-ordonnancement, mais il ne semble pas ... ouch. (Ceci est pour le code MakeFourCC, en utilisant le 2ème tentative de code im encore à ~ 0.143 secondes) – Goz

-3

L'utilisation d'intrinsics brise les optimisations du compilateur!

Tout le but des fonctions intrinsèques est d'insérer des opcodes que le compilateur ne connaît pas dans le flot d'opcodes que le compilateur connaît et a généré. A moins que le compilateur ne reçoive des méta-données sur l'opcode et son impact sur les registres et la mémoire, le compilateur ne peut pas supposer que les données sont préservées après l'exécution de l'intrinsèque. Cela nuit vraiment à la partie optimisation du compilateur - il ne peut pas réorganiser les instructions autour de l'intrinsèque, il ne peut pas supposer que les registres ne sont pas affectés et ainsi de suite.

Je pense que la meilleure façon d'optimiser ceci est de regarder l'image plus grande - vous devez considérer l'ensemble du processus de la lecture des données source à l'écriture de la sortie finale. Les micro optimisations donnent rarement de gros résultats, à moins que vous ne fassiez vraiment quelque chose de mal pour commencer. Peut-être, si vous détailler l'entrée et la sortie requises, quelqu'un pourrait suggérer une méthode optimale pour le gérer.

+7

Je suis assez sûr que les intrinsèques ne cassent pas l'optimisation. C'est tout le point d'entre eux. L'utilisation d'un bloc __asm ​​brise l'optimisation, ce qui explique pourquoi Microsoft a offert des intrinsèques en premier lieu. Ce lien semble être d'accord avec moi ... http://blogs.msdn.com/vcblog/archive/2007/10/18/new-intrinsic-support-in-visual-studio-2008.aspx – Goz

+1

Skizz, avez-vous code SIMD jamais écrit? Personnellement, je préfère éviter les éléments intrinsèques, mais les alternatives sont encore moins portables et plus risquées. – Potatoswatter

+0

@Goz: Je vais éditer mon post, mais, ce que j'ai essayé de dire, c'est que si le compilateur ne sait pas ce qu'est l'intrinsèque, ce serait le même qu'un bloc __asm. Les intrinsèques dans DevStudio peuvent bien être connus du compilateur et donc le compilateur peut optimiser autour d'eux. Si la fonction intrinsèque est juste un wrapper autour d'un bloc __asm ​​alors le compilateur est bloqué incapable de bien optimiser. Si c'est un appel à une bibliothèque, il est inutile de l'utiliser pour optimiser le code. – Skizz

2

VS est notoirement mauvaise pour optimiser les intrinsèques. Déplacement des données en particulier depuis et vers les registres SSE. Les intrinsèques lui-même sont plutôt bien utilisés cependant ....

Ce que vous voyez est qu'il tente de remplir le registre ESS avec ce monstre:

00AA100C movzx  ecx,byte ptr [esp+0Fh] 
00AA1011 movzx  edx,byte ptr [esp+0Fh] 
00AA1016 movzx  eax,byte ptr [esp+0Fh] 
00AA101B movd  xmm0,eax 
00AA101F movzx  eax,byte ptr [esp+0Fh] 
00AA1024 movd  xmm2,edx 
00AA1028 movzx  edx,byte ptr [esp+0Fh] 
00AA102D movd  xmm1,ecx 
00AA1031 movzx  ecx,byte ptr [esp+0Fh] 
00AA1036 movd  xmm4,ecx 
00AA103A movzx  ecx,byte ptr [esp+0Fh] 
00AA103F movd  xmm5,edx 
00AA1043 movzx  edx,byte ptr [esp+0Fh] 
00AA1048 movd  xmm3,eax 
00AA104C movzx  eax,byte ptr [esp+0Fh] 
00AA1051 movdqa  xmmword ptr [esp+60h],xmm0 
00AA1057 movd  xmm0,edx 
00AA105B movzx  edx,byte ptr [esp+0Fh] 
00AA1060 movd  xmm6,eax 
00AA1064 movzx  eax,byte ptr [esp+0Fh] 
00AA1069 movd  xmm7,ecx 
00AA106D movzx  ecx,byte ptr [esp+0Fh] 
00AA1072 movdqa  xmmword ptr [esp+20h],xmm4 
00AA1078 movdqa  xmmword ptr [esp+80h],xmm0 
00AA1081 movd  xmm4,ecx 
00AA1085 movzx  ecx,byte ptr [esp+0Fh] 
00AA108A movdqa  xmmword ptr [esp+70h],xmm2 
00AA1090 movd  xmm0,eax 
00AA1094 movzx  eax,byte ptr [esp+0Fh] 
00AA1099 movdqa  xmmword ptr [esp+10h],xmm4 
00AA109F movdqa  xmmword ptr [esp+50h],xmm6 
00AA10A5 movd  xmm2,edx 
00AA10A9 movzx  edx,byte ptr [esp+0Fh] 
00AA10AE movd  xmm4,eax 
00AA10B2 movzx  eax,byte ptr [esp+0Fh] 
00AA10B7 movd  xmm6,edx 
00AA10BB punpcklbw xmm0,xmm1 
00AA10BF punpcklbw xmm2,xmm3 
00AA10C3 movdqa  xmm3,xmmword ptr [esp+80h] 
00AA10CC movdqa  xmmword ptr [esp+40h],xmm4 
00AA10D2 movd  xmm4,ecx 
00AA10D6 movdqa  xmmword ptr [esp+30h],xmm6 
00AA10DC movdqa  xmm1,xmmword ptr [esp+30h] 
00AA10E2 movd  xmm6,eax 
00AA10E6 punpcklbw xmm4,xmm5 
00AA10EA punpcklbw xmm4,xmm0 
00AA10EE movdqa  xmm0,xmmword ptr [esp+50h] 
00AA10F4 punpcklbw xmm1,xmm0 
00AA10F8 movdqa  xmm0,xmmword ptr [esp+70h] 
00AA10FE punpcklbw xmm6,xmm7 
00AA1102 punpcklbw xmm6,xmm2 
00AA1106 movdqa  xmm2,xmmword ptr [esp+10h] 
00AA110C punpcklbw xmm2,xmm0 
00AA1110 movdqa  xmm0,xmmword ptr [esp+20h] 
00AA1116 punpcklbw xmm1,xmm2 
00AA111A movdqa  xmm2,xmmword ptr [esp+40h] 
00AA1120 punpcklbw xmm2,xmm0 
00AA1124 movdqa  xmm0,xmmword ptr [esp+60h] 
00AA112A punpcklbw xmm3,xmm0 
00AA112E punpcklbw xmm2,xmm3 
00AA1132 punpcklbw xmm6,xmm4 
00AA1136 punpcklbw xmm1,xmm2 
00AA113A punpcklbw xmm6,xmm1 

Cela fonctionne beaucoup mieux et (devrait) être facilement plus rapide:

__declspec(align(16)) BYTE arr[16] = { sample15, sample14, sample13, sample12, sample11, sample10, sample9, sample8, sample7, sample6, sample5, sample4, sample3, sample2, sample1, sample0 }; 

__m128i packedSamples = _mm_load_si128((__m128i*)arr); 

Construire mon propre banc d'essai:

void f() 
{ 
    const int steps = 1000000; 
    BYTE* pDest = new BYTE[steps*16+16]; 
    pDest += 16 - ((ULONG_PTR)pDest % 16); 
    BYTE* pSrc = new BYTE[steps*16*16]; 

    const int channelStep0 = 0; 
    const int channelStep1 = 1; 
    const int channelStep2 = 2; 
    const int channelStep3 = 3; 
    const int channelStep4 = 16; 

    __int64 freq; 
    QueryPerformanceFrequency((LARGE_INTEGER*)&freq); 
    __int64 start = 0, end; 
    QueryPerformanceCounter((LARGE_INTEGER*)&start); 

    for(int step = 0; step < steps; ++step) 
    { 
     __declspec(align(16)) BYTE arr[16]; 
     for(int j = 0; j < 4; ++j) 
     { 
      //for(int i = 0; i < 4; ++i) 
      { 
       arr[0+j*4] = *(pSrc + channelStep0); 
       arr[1+j*4] = *(pSrc + channelStep1); 
       arr[2+j*4] = *(pSrc + channelStep2); 
       arr[3+j*4] = *(pSrc + channelStep3); 
      } 
      pSrc += channelStep4; 
     } 

#if test1 
// test 1 with C 
     for(int i = 0; i < 16; ++i) 
     { 
      *(pDest + step * 16 + i) = arr[i]; 
     } 
#else 
// test 2 with SSE load/store  
     __m128i packedSamples = _mm_load_si128((__m128i*)arr); 
     _mm_stream_si128(((__m128i*)pDest) + step, packedSamples); 
#endif 
    } 

    QueryPerformanceCounter((LARGE_INTEGER*)&end); 

    printf("%I64d", (end - start) * 1000/freq); 

} 

Pour moi, le test 2 est plus rapide que le test 1.

Est-ce que je fais quelque chose de mal? N'est-ce pas le code que vous utilisez? Qu'est-ce qui me manque? Est-ce juste pour moi?

+0

Yup est certainement l'implémentation basée sur SSE la plus rapide jusqu'à présent (~ 0.124 secondes). Mais si vous vérifiez mon dernier montage, vous verrez qu'éviter SSE complètement m'a fourni un boost de vitesse qui bat encore plus haut. Merci beaucoup. C'est toujours très utile. Il y a une raison pour laquelle je préfère juste écrire les trucs sanglants en assembleur;) – Goz

+0

En fait je mens ... J'ai implémenté cela un peu faussement (Doit avoir testé le résultat de l'unité) ... étrangement cela ne fournit aucun boost de vitesse ... il génère à peu près le même code ... – Goz

+0

Et j'essaie une implémentation légèrement différente où chaque chargement est "* (pSamples + = channelStep)", (sauf le premier évidemment) et maintenant je reçois 0.13 secondes ... c'est bien mais toujours pas génial ... – Goz