Une meilleure masortingce 8×8 octets transposée avec SSE?

J’ai trouvé cet article qui explique comment transposer une masortingce de 8 x 8 octets avec 24 opérations, et quelques rouleaux plus tard, il y a le code qui implémente la transposition. Cependant, cette méthode n’exploite pas le fait que nous pouvons bloquer la transposition 8×8 en quatre transposées en 4×4, et chacune d’entre elles peut être réalisée en une seule instruction aléatoire ( cet article est la référence). Alors je suis sorti avec cette solution:

__m128i transpose4x4mask = _mm_set_epi8(15, 11, 7, 3, 14, 10, 6, 2, 13, 9, 5, 1, 12, 8, 4, 0); __m128i shuffle8x8Mask = _mm_setr_epi8(0, 1, 2, 3, 8, 9, 10, 11, 4, 5, 6, 7, 12, 13, 14, 15); void TransposeBlock8x8(uint8_t *src, uint8_t *dst, int srcSsortingde, int dstSsortingde) { __m128i load0 = _mm_set_epi64x(*(uint64_t*)(src + 1 * srcSsortingde), *(uint64_t*)(src + 0 * srcSsortingde)); __m128i load1 = _mm_set_epi64x(*(uint64_t*)(src + 3 * srcSsortingde), *(uint64_t*)(src + 2 * srcSsortingde)); __m128i load2 = _mm_set_epi64x(*(uint64_t*)(src + 5 * srcSsortingde), *(uint64_t*)(src + 4 * srcSsortingde)); __m128i load3 = _mm_set_epi64x(*(uint64_t*)(src + 7 * srcSsortingde), *(uint64_t*)(src + 6 * srcSsortingde)); __m128i shuffle0 = _mm_shuffle_epi8(load0, shuffle8x8Mask); __m128i shuffle1 = _mm_shuffle_epi8(load1, shuffle8x8Mask); __m128i shuffle2 = _mm_shuffle_epi8(load2, shuffle8x8Mask); __m128i shuffle3 = _mm_shuffle_epi8(load3, shuffle8x8Mask); __m128i block0 = _mm_unpacklo_epi64(shuffle0, shuffle1); __m128i block1 = _mm_unpackhi_epi64(shuffle0, shuffle1); __m128i block2 = _mm_unpacklo_epi64(shuffle2, shuffle3); __m128i block3 = _mm_unpackhi_epi64(shuffle2, shuffle3); __m128i transposed0 = _mm_shuffle_epi8(block0, transpose4x4mask); __m128i transposed1 = _mm_shuffle_epi8(block1, transpose4x4mask); __m128i transposed2 = _mm_shuffle_epi8(block2, transpose4x4mask); __m128i transposed3 = _mm_shuffle_epi8(block3, transpose4x4mask); __m128i store0 = _mm_unpacklo_epi32(transposed0, transposed2); __m128i store1 = _mm_unpackhi_epi32(transposed0, transposed2); __m128i store2 = _mm_unpacklo_epi32(transposed1, transposed3); __m128i store3 = _mm_unpackhi_epi32(transposed1, transposed3); *((uint64_t*)(dst + 0 * dstSsortingde)) = _mm_extract_epi64(store0, 0); *((uint64_t*)(dst + 1 * dstSsortingde)) = _mm_extract_epi64(store0, 1); *((uint64_t*)(dst + 2 * dstSsortingde)) = _mm_extract_epi64(store1, 0); *((uint64_t*)(dst + 3 * dstSsortingde)) = _mm_extract_epi64(store1, 1); *((uint64_t*)(dst + 4 * dstSsortingde)) = _mm_extract_epi64(store2, 0); *((uint64_t*)(dst + 5 * dstSsortingde)) = _mm_extract_epi64(store2, 1); *((uint64_t*)(dst + 6 * dstSsortingde)) = _mm_extract_epi64(store3, 0); *((uint64_t*)(dst + 7 * dstSsortingde)) = _mm_extract_epi64(store3, 1); } 

En excluant les opérations de chargement / stockage, cette procédure ne comprend que 16 instructions au lieu de 24.

Qu’est-ce que je rate?

En plus des chargements, des magasins et des pinsrq -s pour lire et écrire en mémoire, avec éventuellement une foulée différente de 8 octets, vous pouvez effectuer la transposition avec seulement 12 instructions (ce code peut facilement être utilisé en combinaison avec le test de Z boson code):

 void tran8x8b_SSE_v2(char *A, char *B) { __m128i pshufbcnst = _mm_set_epi8(15,11,7,3, 14,10,6,2, 13,9,5,1, 12,8,4,0); __m128i B0, B1, B2, B3, T0, T1, T2, T3; B0 = _mm_loadu_si128((__m128i*)&A[ 0]); B1 = _mm_loadu_si128((__m128i*)&A[16]); B2 = _mm_loadu_si128((__m128i*)&A[32]); B3 = _mm_loadu_si128((__m128i*)&A[48]); T0 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B0),_mm_castsi128_ps(B1),0b10001000)); T1 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B2),_mm_castsi128_ps(B3),0b10001000)); T2 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B0),_mm_castsi128_ps(B1),0b11011101)); T3 = _mm_castps_si128(_mm_shuffle_ps(_mm_castsi128_ps(B2),_mm_castsi128_ps(B3),0b11011101)); B0 = _mm_shuffle_epi8(T0,pshufbcnst); B1 = _mm_shuffle_epi8(T1,pshufbcnst); B2 = _mm_shuffle_epi8(T2,pshufbcnst); B3 = _mm_shuffle_epi8(T3,pshufbcnst); T0 = _mm_unpacklo_epi32(B0,B1); T1 = _mm_unpackhi_epi32(B0,B1); T2 = _mm_unpacklo_epi32(B2,B3); T3 = _mm_unpackhi_epi32(B2,B3); _mm_storeu_si128((__m128i*)&B[ 0], T0); _mm_storeu_si128((__m128i*)&B[16], T1); _mm_storeu_si128((__m128i*)&B[32], T2); _mm_storeu_si128((__m128i*)&B[48], T3); } 

Nous utilisons ici le shuffle 32 bits en virgule flottante, qui est plus flexible que le shuffle epi32 . Les conversions ne génèrent pas d’instructions supplémentaires (code généré avec gcc 5.4):

 tran8x8b_SSE_v2: .LFB4885: .cfi_startproc vmovdqu 48(%rdi), %xmm5 vmovdqu 32(%rdi), %xmm2 vmovdqu 16(%rdi), %xmm0 vmovdqu (%rdi), %xmm1 vshufps $136, %xmm5, %xmm2, %xmm4 vshufps $221, %xmm5, %xmm2, %xmm2 vmovdqa .LC6(%rip), %xmm5 vshufps $136, %xmm0, %xmm1, %xmm3 vshufps $221, %xmm0, %xmm1, %xmm1 vpshufb %xmm5, %xmm3, %xmm3 vpshufb %xmm5, %xmm1, %xmm0 vpshufb %xmm5, %xmm4, %xmm4 vpshufb %xmm5, %xmm2, %xmm1 vpunpckldq %xmm4, %xmm3, %xmm5 vpunpckldq %xmm1, %xmm0, %xmm2 vpunpckhdq %xmm4, %xmm3, %xmm3 vpunpckhdq %xmm1, %xmm0, %xmm0 vmovups %xmm5, (%rsi) vmovups %xmm3, 16(%rsi) vmovups %xmm2, 32(%rsi) vmovups %xmm0, 48(%rsi) ret .cfi_endproc 

Sur certains processeurs plus anciens, mais pas tous, il peut exister un léger délai de contournement (entre 0 et 2 cycles) pour le transfert de données entre les unités entière et les unités à virgule flottante. Cela augmente la latence de la fonction, mais n’affecte pas nécessairement le débit du code.

Un test de latence simple avec des transpositions 1e9:

  for (int i=0;i<500000000;i++){ tran8x8b_SSE(A,C); tran8x8b_SSE(C,A); } print8x8b(A); 

Cela prend environ 5,5 secondes (19,7e9 cycles) avec tran8x8b_SSE et 4,5 secondes (16,0e9 cycles) avec tran8x8b_SSE_v2 (Intel Core i5-6500). Notez que le compilateur n'a pas éliminé la charge et les magasins, bien que les fonctions aient été intégrées dans la boucle for.

Mise à jour: Solution AVX2-128 / SSE 4.1 avec mélanges.

Les "shuffles" (déballer, shuffle) sont gérés par le port 5, avec 1 instruction par cycle de processeur sur les processeurs modernes. Parfois, il est rentable de remplacer un «mélange aléatoire» par deux mélanges. Sur Skylake, les instructions de mélange 32 bits peuvent être exécutées sur le port 0, 1 ou 5.

Malheureusement, _mm_blend_epi32 n’est que AVX2-128. Une alternative efficace à SSE 4.1 est _mm_blend_ps en combinaison avec quelques conversions (qui sont généralement gratuites). Les 12 'shuffles' sont remplacés par 8 shuffles en combinaison avec 8 mélanges.

Le test de latence simple s'exécute maintenant en environ 3,6 secondes (13e9 cycles de processeur), soit 18% plus rapide que les résultats obtenus avec tran8x8b_SSE_v2 .

Code:

 /* AVX2-128 version, sse 4.1 version see ----------------> SSE 4.1 version of tran8x8b_AVX2_128() */ void tran8x8b_AVX2_128(char *A, char *B) { /* void tran8x8b_SSE4_1(char *A, char *B) { */ __m128i pshufbcnst_0 = _mm_set_epi8(15, 7,11, 3, 13, 5, 9, 1, 14, 6,10, 2, 12, 4, 8, 0); /* __m128i pshufbcnst_0 = _mm_set_epi8(15, 7,11, 3, 13, 5, 9, 1, 14, 6,10, 2, 12, 4, 8, 0); */ __m128i pshufbcnst_1 = _mm_set_epi8(13, 5, 9, 1, 15, 7,11, 3, 12, 4, 8, 0, 14, 6,10, 2); /* __m128i pshufbcnst_1 = _mm_set_epi8(13, 5, 9, 1, 15, 7,11, 3, 12, 4, 8, 0, 14, 6,10, 2); */ __m128i pshufbcnst_2 = _mm_set_epi8(11, 3,15, 7, 9, 1,13, 5, 10, 2,14, 6, 8, 0,12, 4); /* __m128i pshufbcnst_2 = _mm_set_epi8(11, 3,15, 7, 9, 1,13, 5, 10, 2,14, 6, 8, 0,12, 4); */ __m128i pshufbcnst_3 = _mm_set_epi8( 9, 1,13, 5, 11, 3,15, 7, 8, 0,12, 4, 10, 2,14, 6); /* __m128i pshufbcnst_3 = _mm_set_epi8( 9, 1,13, 5, 11, 3,15, 7, 8, 0,12, 4, 10, 2,14, 6); */ __m128i B0, B1, B2, B3, T0, T1, T2, T3; /* __m128 B0, B1, B2, B3, T0, T1, T2, T3; */ /* */ B0 = _mm_loadu_si128((__m128i*)&A[ 0]); /* B0 = _mm_loadu_ps((float*)&A[ 0]); */ B1 = _mm_loadu_si128((__m128i*)&A[16]); /* B1 = _mm_loadu_ps((float*)&A[16]); */ B2 = _mm_loadu_si128((__m128i*)&A[32]); /* B2 = _mm_loadu_ps((float*)&A[32]); */ B3 = _mm_loadu_si128((__m128i*)&A[48]); /* B3 = _mm_loadu_ps((float*)&A[48]); */ /* */ B1 = _mm_shuffle_epi32(B1,0b10110001); /* B1 = _mm_shuffle_ps(B1,B1,0b10110001); */ B3 = _mm_shuffle_epi32(B3,0b10110001); /* B3 = _mm_shuffle_ps(B3,B3,0b10110001); */ T0 = _mm_blend_epi32(B0,B1,0b1010); /* T0 = _mm_blend_ps(B0,B1,0b1010); */ T1 = _mm_blend_epi32(B2,B3,0b1010); /* T1 = _mm_blend_ps(B2,B3,0b1010); */ T2 = _mm_blend_epi32(B0,B1,0b0101); /* T2 = _mm_blend_ps(B0,B1,0b0101); */ T3 = _mm_blend_epi32(B2,B3,0b0101); /* T3 = _mm_blend_ps(B2,B3,0b0101); */ /* */ B0 = _mm_shuffle_epi8(T0,pshufbcnst_0); /* B0 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T0),pshufbcnst_0)); */ B1 = _mm_shuffle_epi8(T1,pshufbcnst_1); /* B1 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T1),pshufbcnst_1)); */ B2 = _mm_shuffle_epi8(T2,pshufbcnst_2); /* B2 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T2),pshufbcnst_2)); */ B3 = _mm_shuffle_epi8(T3,pshufbcnst_3); /* B3 = _mm_castsi128_ps(_mm_shuffle_epi8(_mm_castps_si128(T3),pshufbcnst_3)); */ /* */ T0 = _mm_blend_epi32(B0,B1,0b1010); /* T0 = _mm_blend_ps(B0,B1,0b1010); */ T1 = _mm_blend_epi32(B0,B1,0b0101); /* T1 = _mm_blend_ps(B0,B1,0b0101); */ T2 = _mm_blend_epi32(B2,B3,0b1010); /* T2 = _mm_blend_ps(B2,B3,0b1010); */ T3 = _mm_blend_epi32(B2,B3,0b0101); /* T3 = _mm_blend_ps(B2,B3,0b0101); */ T1 = _mm_shuffle_epi32(T1,0b10110001); /* T1 = _mm_shuffle_ps(T1,T1,0b10110001); */ T3 = _mm_shuffle_epi32(T3,0b10110001); /* T3 = _mm_shuffle_ps(T3,T3,0b10110001); */ /* */ _mm_storeu_si128((__m128i*)&B[ 0], T0); /* _mm_storeu_ps((float*)&B[ 0], T0); */ _mm_storeu_si128((__m128i*)&B[16], T1); /* _mm_storeu_ps((float*)&B[16], T1); */ _mm_storeu_si128((__m128i*)&B[32], T2); /* _mm_storeu_ps((float*)&B[32], T2); */ _mm_storeu_si128((__m128i*)&B[48], T3); /* _mm_storeu_ps((float*)&B[48], T3); */ } /* } */ 

Poster ceci comme une réponse. Je vais également changer le titre de la question de “… avec SSE” à “… avec SIMD” en raison de certaines réponses et commentaires reçus jusqu’à présent.

J’ai réussi à transposer la masortingce avec AVX2 en 8 instructions uniquement, dont 10 avec chargement / stockage (masques sans charges). EDIT: J’ai trouvé une version plus courte. Voir ci-dessous. C’est le cas où les masortingces sont toutes contiguës en mémoire, un chargement / stockage direct peut donc être utilisé.

Voici le code C:

 void tran8x8b_AVX2(char *src, char *dst) { __m256i perm = _mm256_set_epi8( 0, 0, 0, 7, 0, 0, 0, 5, 0, 0, 0, 3, 0, 0, 0, 1, 0, 0, 0, 6, 0, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0, 0 ); __m256i tm = _mm256_set_epi8( 15, 11, 7, 3, 14, 10, 6, 2, 13, 9, 5, 1, 12, 8, 4, 0, 15, 11, 7, 3, 14, 10, 6, 2, 13, 9, 5, 1, 12, 8, 4, 0 ); __m256i load0 = _mm256_loadu_si256((__m256i*)&src[ 0]); __m256i load1 = _mm256_loadu_si256((__m256i*)&src[32]); __m256i perm0 = _mm256_permutevar8x32_epi32(load0, perm); __m256i perm1 = _mm256_permutevar8x32_epi32(load1, perm); __m256i transpose0 = _mm256_shuffle_epi8(perm0, tm); __m256i transpose1 = _mm256_shuffle_epi8(perm1, tm); __m256i unpack0 = _mm256_unpacklo_epi32(transpose0, transpose1); __m256i unpack1 = _mm256_unpackhi_epi32(transpose0, transpose1); perm0 = _mm256_castps_si256(_mm256_permute2f128_ps(_mm256_castsi256_ps(unpack0), _mm256_castsi256_ps(unpack1), 32)); perm1 = _mm256_castps_si256(_mm256_permute2f128_ps(_mm256_castsi256_ps(unpack0), _mm256_castsi256_ps(unpack1), 49)); _mm256_storeu_si256((__m256i*)&dst[ 0], perm0); _mm256_storeu_si256((__m256i*)&dst[32], perm1); } 

GCC était assez intelligent pour effectuer une permutation pendant le chargement d’AVX, en sauvegardant deux instructions. Voici la sortie du compilateur:

 tran8x8b_AVX2(char*, char*): vmovdqa ymm1, YMMWORD PTR .LC0[rip] vmovdqa ymm2, YMMWORD PTR .LC1[rip] vpermd ymm0, ymm1, YMMWORD PTR [rdi] vpermd ymm1, ymm1, YMMWORD PTR [rdi+32] vpshufb ymm0, ymm0, ymm2 vpshufb ymm1, ymm1, ymm2 vpunpckldq ymm2, ymm0, ymm1 vpunpckhdq ymm0, ymm0, ymm1 vinsertf128 ymm1, ymm2, xmm0, 1 vperm2f128 ymm0, ymm2, ymm0, 49 vmovdqu YMMWORD PTR [rsi], ymm1 vmovdqu YMMWORD PTR [rsi+32], ymm0 vzeroupper ret 

Il a émis l’instruction vzerupper avec -O3, mais descendre à -O1 supprime cela.

Dans le cas de mon problème initial (une grande masortingce et un zoom sur une partie 8×8 de celle-ci), la gestion des pas détruit la sortie de manière assez mauvaise:

 void tran8x8b_AVX2(char *src, char *dst, int srcSsortingde, int dstSsortingde) { __m256i load0 = _mm256_set_epi64x(*(uint64_t*)(src + 3 * srcSsortingde), *(uint64_t*)(src + 2 * srcSsortingde), *(uint64_t*)(src + 1 * srcSsortingde), *(uint64_t*)(src + 0 * srcSsortingde)); __m256i load1 = _mm256_set_epi64x(*(uint64_t*)(src + 7 * srcSsortingde), *(uint64_t*)(src + 6 * srcSsortingde), *(uint64_t*)(src + 5 * srcSsortingde), *(uint64_t*)(src + 4 * srcSsortingde)); // ... the same as before, however we can skip the final permutations because we need to handle the destination ssortingde... *((uint64_t*)(dst + 0 * dstSsortingde)) = _mm256_extract_epi64(unpack0, 0); *((uint64_t*)(dst + 1 * dstSsortingde)) = _mm256_extract_epi64(unpack0, 1); *((uint64_t*)(dst + 2 * dstSsortingde)) = _mm256_extract_epi64(unpack1, 0); *((uint64_t*)(dst + 3 * dstSsortingde)) = _mm256_extract_epi64(unpack1, 1); *((uint64_t*)(dst + 4 * dstSsortingde)) = _mm256_extract_epi64(unpack0, 2); *((uint64_t*)(dst + 5 * dstSsortingde)) = _mm256_extract_epi64(unpack0, 3); *((uint64_t*)(dst + 6 * dstSsortingde)) = _mm256_extract_epi64(unpack1, 2); *((uint64_t*)(dst + 7 * dstSsortingde)) = _mm256_extract_epi64(unpack1, 3); } 

Voici la sortie du compilateur:

 tran8x8b_AVX2(char*, char*, int, int): movsx rdx, edx vmovq xmm5, QWORD PTR [rdi] lea r9, [rdi+rdx] vmovdqa ymm3, YMMWORD PTR .LC0[rip] movsx rcx, ecx lea r11, [r9+rdx] vpinsrq xmm0, xmm5, QWORD PTR [r9], 1 lea r10, [r11+rdx] vmovq xmm4, QWORD PTR [r11] vpinsrq xmm1, xmm4, QWORD PTR [r10], 1 lea r8, [r10+rdx] lea rax, [r8+rdx] vmovq xmm7, QWORD PTR [r8] vmovq xmm6, QWORD PTR [rax+rdx] vpinsrq xmm2, xmm7, QWORD PTR [rax], 1 vinserti128 ymm1, ymm0, xmm1, 0x1 vpinsrq xmm0, xmm6, QWORD PTR [rax+rdx*2], 1 lea rax, [rsi+rcx] vpermd ymm1, ymm3, ymm1 vinserti128 ymm0, ymm2, xmm0, 0x1 vmovdqa ymm2, YMMWORD PTR .LC1[rip] vpshufb ymm1, ymm1, ymm2 vpermd ymm0, ymm3, ymm0 vpshufb ymm0, ymm0, ymm2 vpunpckldq ymm2, ymm1, ymm0 vpunpckhdq ymm0, ymm1, ymm0 vmovdqa xmm1, xmm2 vmovq QWORD PTR [rsi], xmm1 vpextrq QWORD PTR [rax], xmm1, 1 vmovdqa xmm1, xmm0 add rax, rcx vextracti128 xmm0, ymm0, 0x1 vmovq QWORD PTR [rax], xmm1 add rax, rcx vpextrq QWORD PTR [rax], xmm1, 1 add rax, rcx vextracti128 xmm1, ymm2, 0x1 vmovq QWORD PTR [rax], xmm1 add rax, rcx vpextrq QWORD PTR [rax], xmm1, 1 vmovq QWORD PTR [rax+rcx], xmm0 vpextrq QWORD PTR [rax+rcx*2], xmm0, 1 vzeroupper ret 

Cependant, cela ne semble pas un gros problème si on le compare à la sortie de mon code original.


EDIT: J’ai trouvé une version plus courte. 4 instructions au total, 8 en comptant les charges / magasins. Ceci est possible parce que je lis la masortingce différemment en cachant des “shuffles” dans l’instruction “rassembler” lors du chargement. Notez également que la permutation finale est nécessaire pour effectuer la sauvegarde car AVX2 n’a pas d’instruction “scatter”. Avoir une instruction de dispersion réduirait tout à 2 instructions seulement. En outre, notez que je peux gérer sans tracas la foulée de src en modifiant le contenu du vecteur vindex .

Malheureusement, cet AVX_v2 semble être plus lent que le précédent. Voici le code:

 void tran8x8b_AVX2_v2(char *src1, char *dst1) { __m256i tm = _mm256_set_epi8( 15, 11, 7, 3, 14, 10, 6, 2, 13, 9, 5, 1, 12, 8, 4, 0, 15, 11, 7, 3, 14, 10, 6, 2, 13, 9, 5, 1, 12, 8, 4, 0 ); __m256i vindex = _mm256_setr_epi32(0, 8, 16, 24, 32, 40, 48, 56); __m256i perm = _mm256_setr_epi32(0, 4, 1, 5, 2, 6, 3, 7); __m256i load0 = _mm256_i32gather_epi32((int*)src1, vindex, 1); __m256i load1 = _mm256_i32gather_epi32((int*)(src1 + 4), vindex, 1); __m256i transpose0 = _mm256_shuffle_epi8(load0, tm); __m256i transpose1 = _mm256_shuffle_epi8(load1, tm); __m256i final0 = _mm256_permutevar8x32_epi32(transpose0, perm); __m256i final1 = _mm256_permutevar8x32_epi32(transpose1, perm); _mm256_storeu_si256((__m256i*)&dst1[ 0], final0); _mm256_storeu_si256((__m256i*)&dst1[32], final1); } 

Et voici la sortie du compilateur:

 tran8x8b_AVX2_v2(char*, char*): vpcmpeqd ymm3, ymm3, ymm3 vmovdqa ymm2, YMMWORD PTR .LC0[rip] vmovdqa ymm4, ymm3 vpgatherdd ymm0, DWORD PTR [rdi+4+ymm2*8], ymm3 vpgatherdd ymm1, DWORD PTR [rdi+ymm2*8], ymm4 vmovdqa ymm2, YMMWORD PTR .LC1[rip] vpshufb ymm1, ymm1, ymm2 vpshufb ymm0, ymm0, ymm2 vmovdqa ymm2, YMMWORD PTR .LC2[rip] vpermd ymm1, ymm2, ymm1 vpermd ymm0, ymm2, ymm0 vmovdqu YMMWORD PTR [rsi], ymm1 vmovdqu YMMWORD PTR [rsi+32], ymm0 vzeroupper ret 

Normalement, lorsque les instructions de chargement et de stockage ne sont pas comptées, c’est parce que le code fonctionne avec une masortingce dans un registre, par exemple en effectuant plusieurs opérations en plus de la transposition dans une boucle. Les charges et les magasins dans ce cas ne sont pas comptés car ils ne font pas partie de la boucle principale.

Mais dans votre code, les charges et les magasins (ou plutôt les ensembles et les extraits) font partie de la transposition.

GCC implémente _mm_set_epi64x pour SSE4.1 dans votre code avec _mm_insert_epi64 et _mm_loadl_epi64 . L’instruction d’insertion fait partie de la transposition, c’est-à-dire que la transposition commence à la charge load0,1,2,3 non à la shuffle0,1,2,3 . Et puis vos dernières valeurs store0,1,2,3 ne contiennent pas non plus la transposition. Vous devez utiliser huit instructions _mm_extract_epi64 pour terminer la transposition en mémoire. Il n’a donc aucun sens de ne pas compter l’ensemble et d’extraire les éléments insortingnsèques.

Dans tous les cas, il s’avère que vous pouvez faire la transposition à partir du registre avec seulement 16 instructions en utilisant uniquement SSSE3, comme ceci:

 //__m128i B0, __m128i B1, __m128i B2, __m128i B3 __m128i mask = _mm_setr_epi8(0x0,0x04,0x01,0x05, 0x02,0x06,0x03,0x07, 0x08,0x0c,0x09,0x0d, 0x0a,0x0e,0x0b,0x0f); __m128i T0, T1, T2, T3; T0 = _mm_unpacklo_epi8(B0,B1); T1 = _mm_unpackhi_epi8(B0,B1); T2 = _mm_unpacklo_epi8(B2,B3); T3 = _mm_unpackhi_epi8(B2,B3); B0 = _mm_unpacklo_epi16(T0,T2); B1 = _mm_unpackhi_epi16(T0,T2); B2 = _mm_unpacklo_epi16(T1,T3); B3 = _mm_unpackhi_epi16(T1,T3); T0 = _mm_unpacklo_epi32(B0,B2); T1 = _mm_unpackhi_epi32(B0,B2); T2 = _mm_unpacklo_epi32(B1,B3); T3 = _mm_unpackhi_epi32(B1,B3); B0 = _mm_shuffle_epi8(T0,mask); B1 = _mm_shuffle_epi8(T1,mask); B2 = _mm_shuffle_epi8(T2,mask); B3 = _mm_shuffle_epi8(T3,mask); 

Je ne sais pas s’il est logique d’exclure les charges et de stocker ici, car je ne sais pas s’il est pratique de travailler avec une masortingce de 8 x 8 octets dans quatre registres de 128 bits.

Voici le code qui teste ceci:

 #include  #include  void print8x8b(char *A) { for(int i=0; i<8; i++) { for(int j=0; j<8; j++) { printf("%2d ", A[i*8+j]); } puts(""); } puts(""); } void tran8x8b(char *A, char *B) { for(int i=0; i<8; i++) { for(int j=0; j<8; j++) { B[j*8+i] = A[i*8+j]; } } } void tran8x8b_SSE(char *A, char *B) { __m128i mask = _mm_setr_epi8(0x0,0x04,0x01,0x05, 0x02,0x06,0x03,0x07, 0x08,0x0c,0x09,0x0d, 0x0a,0x0e,0x0b,0x0f); __m128i B0, B1, B2, B3, T0, T1, T2, T3; B0 = _mm_loadu_si128((__m128i*)&A[ 0]); B1 = _mm_loadu_si128((__m128i*)&A[16]); B2 = _mm_loadu_si128((__m128i*)&A[32]); B3 = _mm_loadu_si128((__m128i*)&A[48]); T0 = _mm_unpacklo_epi8(B0,B1); T1 = _mm_unpackhi_epi8(B0,B1); T2 = _mm_unpacklo_epi8(B2,B3); T3 = _mm_unpackhi_epi8(B2,B3); B0 = _mm_unpacklo_epi16(T0,T2); B1 = _mm_unpackhi_epi16(T0,T2); B2 = _mm_unpacklo_epi16(T1,T3); B3 = _mm_unpackhi_epi16(T1,T3); T0 = _mm_unpacklo_epi32(B0,B2); T1 = _mm_unpackhi_epi32(B0,B2); T2 = _mm_unpacklo_epi32(B1,B3); T3 = _mm_unpackhi_epi32(B1,B3); B0 = _mm_shuffle_epi8(T0,mask); B1 = _mm_shuffle_epi8(T1,mask); B2 = _mm_shuffle_epi8(T2,mask); B3 = _mm_shuffle_epi8(T3,mask); _mm_storeu_si128((__m128i*)&B[ 0], B0); _mm_storeu_si128((__m128i*)&B[16], B1); _mm_storeu_si128((__m128i*)&B[32], B2); _mm_storeu_si128((__m128i*)&B[48], B3); } int main(void) { char A[64], B[64], C[64]; for(int i=0; i<64; i++) A[i] = i; print8x8b(A); tran8x8b(A,B); print8x8b(B); tran8x8b_SSE(A,C); print8x8b(C); } 

Un simplifié

 void tp128_8x8(char *A, char *B) { __m128i sv = _mm_set_epi8(15, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 0); __m128i iv[4], ov[4]; ov[0] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)A), sv); ov[1] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+16)), sv); ov[2] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+32)), sv); ov[3] = _mm_shuffle_epi8(_mm_loadu_si128((__m128i*)(A+48)), sv); iv[0] = _mm_unpacklo_epi16(ov[0], ov[1]); iv[1] = _mm_unpackhi_epi16(ov[0], ov[1]); iv[2] = _mm_unpacklo_epi16(ov[2], ov[3]); iv[3] = _mm_unpackhi_epi16(ov[2], ov[3]); _mm_storeu_si128((__m128i*)B, _mm_unpacklo_epi32(iv[0], iv[2])); _mm_storeu_si128((__m128i*)(B+16), _mm_unpackhi_epi32(iv[0], iv[2])); _mm_storeu_si128((__m128i*)(B+32), _mm_unpacklo_epi32(iv[1], iv[3])); _mm_storeu_si128((__m128i*)(B+48), _mm_unpackhi_epi32(iv[1], iv[3])); } Benchmark:i5-5300U 2.3GHz (cycles per byte) tran8x8b : 2.140 tran8x8b_SSE : 1.602 tran8x8b_SSE_v2 : 1.551 tp128_8x8 : 1.535 tran8x8b_AVX2 : 1.563 tran8x8b_AVX2_v2 : 1.731