VC ++ ESS bizarreries d'optimisation intrinsèque
-
18-09-2019 - |
Question
Je l'exécution d'une lecture dispersée de données de 8 bits à partir d'un fichier (désentrelacement un fichier d'onde de canal 64). Je suis alors les combiner pour être un seul flux d'octets. Le problème que je vais avoir est avec ma re-construction des données à écrire.
En fait, je lis dans 16 octets, puis les intégrer dans une seule variable __m128i puis en utilisant _mm_stream_ps pour écrire la valeur de retour vers la mémoire. Cependant, j'ai des résultats de performance impairs.
Dans mon premier schéma j'utilise le _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 );
En fait, je laisse le tout au compilateur de décider comment l'optimiser pour obtenir la meilleure performance. Cela donne plus Mauvaise performance. Mon test fonctionne en 0.195 secondes ~.
Deuxièmement, j'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éliorer les performances un peu. Mon test fonctionne maintenant dans ~ 0,15 secondes. Il semble contre-intuitif que la performance améliorerait en faisant cela que je suppose que c'est exactement ce que _mm_set_epi8 est en train de faire de toute façon ...
Ma dernière tentative était d'utiliser un peu de code que j'ai de faire quatre CCs l'ancienne façon fashioned (avec des changements et ORS), puis 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 à se confondre.
J'ai donc essayé un simple système d'écriture d'octets d'octets de lecture et qui est toujours aussi un peu plus vite que même la dernière méthode.
Alors qu'est-ce qui se passe? Cela me semble tout contre-intuitif.
Je l'ai considéré l'idée que les retards sont les INTERVENUES _mm_stream_ps parce que je fournir des données trop rapidement, mais alors je pour obtenir exactement les mêmes résultats sur tout ce que je fais. Est-il possible que les 2 premières méthodes signifient que les 16 charges peuvent pas distribuées à travers la boucle pour cacher la latence? Si oui, pourquoi est-ce? Certes, un intrinsèque permet au compilateur de faire Optimisations au fur et où il veut .. Je pensais que tout était le point ... Aussi EFFECTUER sûrement 16 lectures et 16 écritures seront beaucoup plus lent que 16 lectures et 1 écriture avec un groupe de jonglage SSE instructions ... Après tout c'est le lit et écrit qui sont le peu lent!
Toute personne ayant des idées sur 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 du droit C ...
Modifier Encore une fois: La meilleure performance que je reçois est à ce jour
// 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 mais ... Je suis encore confus par cela, mais .. ho hum.
La solution
Peut-être que le compilateur essaie de mettre tous les arguments à la valeur intrinsèque dans des registres à la fois. Vous ne voulez pas accéder que de nombreuses variables à la fois sans les organiser.
Plutôt que de déclarer un identifiant distinct pour chaque échantillon, essayer de les mettre dans un char[16]
. Le compilateur favorisera les 16 valeurs de registres comme il l'entend, aussi longtemps que vous ne prenez pas l'adresse de quoi que ce soit dans le tableau. Vous pouvez ajouter une balise __aligned__
(ou tout autre utilisation VC ++) et peut-être éviter le tout à fait intrinsèque. Dans le cas contraire, appeler la valeur intrinsèque avec ( sample[15], sample[14], sample[13] … sample[0] )
devrait rendre le travail du compilateur plus facile ou du moins ne pas nuire.
Modifier 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 de votre dernière tentative entrelacer (en utilisant MakeFourCC) avec les opérations de lecture, pour vous assurer qu'il est prévu correctement et sans allers-retours à la pile. Bien sûr, l'inspection du code objet est la meilleure façon de faire en sorte que.
Pour l'essentiel, vous diffusez des données dans le fichier de registre et le streaming en arrière sur. Vous ne voulez pas surcharger avant qu'il est temps de vider les données.
Autres conseils
VS est notoirement mauvaise à optimiser intrinsics. En particulier, le déplacement des données depuis et vers les registres SSE. Les intrinsics lui-même sont utilisés assez bien mais ....
Ce que vous voyez est qu'il tente de remplir le registre SSE 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 rapidement:
__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 );
Créer mon 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 test 2 est plus rapide que le test 1.
Est-ce que je fais quelque chose de mal? Est-ce pas le code que vous utilisez? Que dois-je manquer? Est-ce juste pour moi?
Utilisation Optimisations du compilateur de pauses intrinsèques!
Le point de l'ensemble des fonctions intrinsèques est d'insérer opcodes le compilateur ne connaît pas dans le flux de opcodes le compilateur ne sait au sujet et a généré. À moins que le compilateur est donné des méta-données sur l'opcode et comment elle affecte les registres et la mémoire, le compilateur ne peut supposer que toutes les données sont conservées après l'exécution de la valeur intrinsèque. Cela fait vraiment mal la partie optimisation du compilateur -. Il ne peut pas modifier l'ordre des instructions autour de la valeur intrinsèque, il ne peut pas assumer les registres ne sont pas affectés et ainsi de suite
Je pense que la meilleure façon d'optimiser est de regarder le tableau d'ensemble - vous devez considérer le processus de lecture des données de source pour écrire la sortie finale. Micro donnent rarement de grands Optimisations résultats, à moins que vous faites quelque chose de vraiment mal pour commencer.
Peut-être, si vous détaillez la personne d'entrée et de sortie requis ici pourrait suggérer une méthode optimale pour le manipuler.