Multiplication masortingcielle 4×4 efficace (assemblage C vs)

Je cherche un moyen plus rapide et plus délicat de multiplier deux masortingces 4×4 en C. Mes recherches actuelles portent sur l’assemblage x86-64 avec des extensions SIMD. Jusqu’à présent, j’ai créé une fonction qui est environ 6 fois plus rapide qu’une implémentation C naïve, ce qui a dépassé mes attentes en termes d’amélioration des performances. Malheureusement, cela n’est vrai que si aucun indicateur d’optimisation n’est utilisé pour la compilation (GCC 4.7). Avec -O2 , C devient plus rapide et mon effort perd tout son sens.

Je sais que les compilateurs modernes utilisent des techniques d’optimisation complexes pour obtenir un code presque parfait, généralement plus rapide qu’un ingénieux assemblage assemblé à la main. Mais dans une minorité de cas critiques en termes de performances, un humain peut essayer de lutter pour des cycles d’horloge avec le compilateur. Surtout quand on peut explorer certaines mathématiques adossées à un ISA moderne (comme c’est le cas dans mon cas).

Ma fonction a l’aspect suivant (syntaxe AT & T, GNU Assembler):

  .text .globl masortingxMultiplyASM .type masortingxMultiplyASM, @function masortingxMultiplyASM: movaps (%rdi), %xmm0 # fetch the first masortingx (use four registers) movaps 16(%rdi), %xmm1 movaps 32(%rdi), %xmm2 movaps 48(%rdi), %xmm3 xorq %rcx, %rcx # reset (forward) loop iterator .ROW: movss (%rsi), %xmm4 # Compute four values (one row) in parallel: shufps $0x0, %xmm4, %xmm4 # 4x 4FP mul's, 3x 4FP add's 6x mov's per row, mulps %xmm0, %xmm4 # expressed in four sequences of 5 instructions, movaps %xmm4, %xmm5 # executed 4 times for 1 masortingx multiplication. addq $0x4, %rsi movss (%rsi), %xmm4 # movss + shufps comprise _mm_set1_ps insortingnsic shufps $0x0, %xmm4, %xmm4 # mulps %xmm1, %xmm4 addps %xmm4, %xmm5 addq $0x4, %rsi # manual pointer arithmetic simplifies addressing movss (%rsi), %xmm4 shufps $0x0, %xmm4, %xmm4 mulps %xmm2, %xmm4 # actual computation happens here addps %xmm4, %xmm5 # addq $0x4, %rsi movss (%rsi), %xmm4 # one mulps operand fetched per sequence shufps $0x0, %xmm4, %xmm4 # | mulps %xmm3, %xmm4 # the other is already waiting in %xmm[0-3] addps %xmm4, %xmm5 addq $0x4, %rsi # 5 preceding comments ssortingde among the 4 blocks movaps %xmm5, (%rdx,%rcx) # store the resulting row, actually, a column addq $0x10, %rcx # (masortingces are stored in column-major order) cmpq $0x40, %rcx jne .ROW ret .size masortingxMultiplyASM, .-masortingxMultiplyASM 

Il calcule une colonne entière de la masortingce résultante par itération, en traitant quatre flottants conditionnés dans des registres SSE 128 bits. La vectorisation complète est possible avec un peu d’instructions mathématiques (réorganisation et agrégation d’opérations) et des instructions mullps / addps pour la multiplication / l’ajout parallèle de paquets 4xfloat. Le code réutilise les registres destinés à la transmission de parameters ( %rdi , %rsi , %rdx : ABI GNU / Linux), bénéficie du déroulement de boucle (interne) et contient une masortingce entièrement dans des registres XMM afin de réduire les lectures en mémoire. A vous de voir, j’ai étudié le sujet et pris mon temps pour le mettre en œuvre du mieux que je peux.

Le calcul du C naïf qui conquiert mon code ressemble à ceci:

 void masortingxMultiplyNormal(mat4_t *mat_a, mat4_t *mat_b, mat4_t *mat_r) { for (unsigned int i = 0; i < 16; i += 4) for (unsigned int j = 0; j m[i + j] = (mat_b->m[i + 0] * mat_a->m[j + 0]) + (mat_b->m[i + 1] * mat_a->m[j + 4]) + (mat_b->m[i + 2] * mat_a->m[j + 8]) + (mat_b->m[i + 3] * mat_a->m[j + 12]); } 

J’ai étudié la sortie d’assemblage optimisée du code C susmentionné qui, tout en stockant des flottants dans des registres XMM, n’implique aucune opération parallèle – il ne s’agit que de calculs scalaires, d’arithmétique de pointeur et de sauts conditionnels. Le code du compilateur semble être moins délibéré, mais il est toujours légèrement plus efficace que ma version vectorisée devrait être environ 4x plus rapide. Je suis sûr que l’idée générale est la bonne: les programmeurs font des choses similaires avec des résultats enrichissants. Mais qu’est-ce qui ne va pas ici? Existe-t-il des problèmes d’allocation de registre ou de planification des instructions dont je ne suis pas au courant? Connaissez-vous des outils d’assemblage x86-64 ou des astuces pour soutenir mon combat contre la machine?

La multiplication masortingcielle 4×4 est de 64 multiplications et 48 additions. En utilisant SSE, cela peut être réduit à 16 multiplications et 12 additions (et 16 émissions). Le code suivant le fera pour vous. Il ne nécessite que SSE ( #include ). Les tableaux A , B et C doivent être alignés sur 16 octets. L’utilisation d’instructions horizontales telles que hadd (SSE3) et dpps (SSE4.1) sera moins efficace (en particulier dpps ). Je ne sais pas si le déroulement de la boucle aidera.

 void M4x4_SSE(float *A, float *B, float *C) { __m128 row1 = _mm_load_ps(&B[0]); __m128 row2 = _mm_load_ps(&B[4]); __m128 row3 = _mm_load_ps(&B[8]); __m128 row4 = _mm_load_ps(&B[12]); for(int i=0; i<4; i++) { __m128 brod1 = _mm_set1_ps(A[4*i + 0]); __m128 brod2 = _mm_set1_ps(A[4*i + 1]); __m128 brod3 = _mm_set1_ps(A[4*i + 2]); __m128 brod4 = _mm_set1_ps(A[4*i + 3]); __m128 row = _mm_add_ps( _mm_add_ps( _mm_mul_ps(brod1, row1), _mm_mul_ps(brod2, row2)), _mm_add_ps( _mm_mul_ps(brod3, row3), _mm_mul_ps(brod4, row4))); _mm_store_ps(&C[4*i], row); } } 

Il existe un moyen d’accélérer le code et de surpasser le compilateur. Cela n’implique aucune parsing de pipeline sophistiquée ni micro-optimisation approfondie du code (cela ne signifie pas qu’il ne pourrait plus en tirer profit). L’optimisation utilise trois astuces simples:

  1. La fonction est maintenant alignée sur 32 octets (ce qui a considérablement amélioré les performances),

  2. La boucle principale est inversée, ce qui réduit la comparaison à un test à zéro (basé sur EFLAGS),

  3. L’arithmétique d’adresse au niveau instruction s’est avérée plus rapide que le calcul du pointeur “externe” (même s’il nécessite deux fois plus d’ajouts “dans 3/4 cas”). Il a raccourci le corps de la boucle de quatre instructions et réduit les dépendances de données dans son chemin d’exécution. Voir la question connexe .

De plus, le code utilise une syntaxe de saut relatif qui supprime les erreurs de redéfinition de symboles, ce qui se produit lorsque GCC tente de les insérer en ligne (après avoir été placées dans une instruction asm et compilées avec -O3 ).

  .text .align 32 # 1. function entry alignment .globl masortingxMultiplyASM # (for a faster call) .type masortingxMultiplyASM, @function masortingxMultiplyASM: movaps (%rdi), %xmm0 movaps 16(%rdi), %xmm1 movaps 32(%rdi), %xmm2 movaps 48(%rdi), %xmm3 movq $48, %rcx # 2. loop reversal 1: # (for simpler exit condition) movss (%rsi, %rcx), %xmm4 # 3. extended address operands shufps $0, %xmm4, %xmm4 # (faster than pointer calculation) mulps %xmm0, %xmm4 movaps %xmm4, %xmm5 movss 4(%rsi, %rcx), %xmm4 shufps $0, %xmm4, %xmm4 mulps %xmm1, %xmm4 addps %xmm4, %xmm5 movss 8(%rsi, %rcx), %xmm4 shufps $0, %xmm4, %xmm4 mulps %xmm2, %xmm4 addps %xmm4, %xmm5 movss 12(%rsi, %rcx), %xmm4 shufps $0, %xmm4, %xmm4 mulps %xmm3, %xmm4 addps %xmm4, %xmm5 movaps %xmm5, (%rdx, %rcx) subq $16, %rcx # one 'sub' (vs 'add' & 'cmp') jge 1b # SF=OF, idiom: jump if positive ret 

C’est la mise en oeuvre x86-64 la plus rapide que j’ai vue jusqu’à présent. J’apprécierai, voterai et accepterai toute réponse fournissant une pièce de assembly plus rapide à cette fin!

Je me demande si la transposition d’une des masortingces peut être bénéfique.

Considérez comment nous multiplions les deux masortingces suivantes …

 A1 A2 A3 A4 W1 W2 W3 W4 B1 B2 B3 B4 X1 X2 X3 X4 C1 C2 C3 C4 * Y1 Y2 Y3 Y4 D1 D2 D3 D4 Z1 Z2 Z3 Z4 

Cela se traduirait par …

 dot(A,?1) dot(A,?2) dot(A,?3) dot(A,?4) dot(B,?1) dot(B,?2) dot(B,?3) dot(B,?4) dot(C,?1) dot(C,?2) dot(C,?3) dot(C,?4) dot(D,?1) dot(D,?2) dot(D,?3) dot(D,?4) 

Faire le produit scalaire d’une ligne et d’une colonne est une douleur.

Et si on transposait la deuxième masortingce avant de multiplier?

 A1 A2 A3 A4 W1 X1 Y1 Z1 B1 B2 B3 B4 W2 X2 Y2 Z2 C1 C2 C3 C4 * W3 X3 Y3 Z3 D1 D2 D3 D4 W4 X4 Y4 Z4 

Maintenant, au lieu de faire le produit scalaire d’une ligne et d’une colonne, nous faisons le produit scalaire de deux lignes. Cela pourrait se prêter à une meilleure utilisation des instructions SIMD.

J’espère que cela t’aides.

Sandy Bridge ci-dessus étend le jeu d’instructions pour prendre en charge l’arithmétique des vecteurs à 8 éléments. Considérez cette implémentation.

 struct MATRIX { union { float f[4][4]; __m128 m[4]; __m256 n[2]; }; }; MATRIX myMultiply(MATRIX M1, MATRIX M2) { // Perform a 4x4 masortingx multiply by a 4x4 masortingx // Be sure to run in 64 bit mode and set right flags // Properties, C/C++, Enable Enhanced Instruction, /arch:AVX // Having MATRIX on a 32 byte bundry does help performance MATRIX mResult; __m256 a0, a1, b0, b1; __m256 c0, c1, c2, c3, c4, c5, c6, c7; __m256 t0, t1, u0, u1; t0 = M1.n[0]; // t0 = a00, a01, a02, a03, a10, a11, a12, a13 t1 = M1.n[1]; // t1 = a20, a21, a22, a23, a30, a31, a32, a33 u0 = M2.n[0]; // u0 = b00, b01, b02, b03, b10, b11, b12, b13 u1 = M2.n[1]; // u1 = b20, b21, b22, b23, b30, b31, b32, b33 a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(0, 0, 0, 0)); // a0 = a00, a00, a00, a00, a10, a10, a10, a10 a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(0, 0, 0, 0)); // a1 = a20, a20, a20, a20, a30, a30, a30, a30 b0 = _mm256_permute2f128_ps(u0, u0, 0x00); // b0 = b00, b01, b02, b03, b00, b01, b02, b03 c0 = _mm256_mul_ps(a0, b0); // c0 = a00*b00 a00*b01 a00*b02 a00*b03 a10*b00 a10*b01 a10*b02 a10*b03 c1 = _mm256_mul_ps(a1, b0); // c1 = a20*b00 a20*b01 a20*b02 a20*b03 a30*b00 a30*b01 a30*b02 a30*b03 a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(1, 1, 1, 1)); // a0 = a01, a01, a01, a01, a11, a11, a11, a11 a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(1, 1, 1, 1)); // a1 = a21, a21, a21, a21, a31, a31, a31, a31 b0 = _mm256_permute2f128_ps(u0, u0, 0x11); // b0 = b10, b11, b12, b13, b10, b11, b12, b13 c2 = _mm256_mul_ps(a0, b0); // c2 = a01*b10 a01*b11 a01*b12 a01*b13 a11*b10 a11*b11 a11*b12 a11*b13 c3 = _mm256_mul_ps(a1, b0); // c3 = a21*b10 a21*b11 a21*b12 a21*b13 a31*b10 a31*b11 a31*b12 a31*b13 a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(2, 2, 2, 2)); // a0 = a02, a02, a02, a02, a12, a12, a12, a12 a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(2, 2, 2, 2)); // a1 = a22, a22, a22, a22, a32, a32, a32, a32 b1 = _mm256_permute2f128_ps(u1, u1, 0x00); // b0 = b20, b21, b22, b23, b20, b21, b22, b23 c4 = _mm256_mul_ps(a0, b1); // c4 = a02*b20 a02*b21 a02*b22 a02*b23 a12*b20 a12*b21 a12*b22 a12*b23 c5 = _mm256_mul_ps(a1, b1); // c5 = a22*b20 a22*b21 a22*b22 a22*b23 a32*b20 a32*b21 a32*b22 a32*b23 a0 = _mm256_shuffle_ps(t0, t0, _MM_SHUFFLE(3, 3, 3, 3)); // a0 = a03, a03, a03, a03, a13, a13, a13, a13 a1 = _mm256_shuffle_ps(t1, t1, _MM_SHUFFLE(3, 3, 3, 3)); // a1 = a23, a23, a23, a23, a33, a33, a33, a33 b1 = _mm256_permute2f128_ps(u1, u1, 0x11); // b0 = b30, b31, b32, b33, b30, b31, b32, b33 c6 = _mm256_mul_ps(a0, b1); // c6 = a03*b30 a03*b31 a03*b32 a03*b33 a13*b30 a13*b31 a13*b32 a13*b33 c7 = _mm256_mul_ps(a1, b1); // c7 = a23*b30 a23*b31 a23*b32 a23*b33 a33*b30 a33*b31 a33*b32 a33*b33 c0 = _mm256_add_ps(c0, c2); // c0 = c0 + c2 (two terms, first two rows) c4 = _mm256_add_ps(c4, c6); // c4 = c4 + c6 (the other two terms, first two rows) c1 = _mm256_add_ps(c1, c3); // c1 = c1 + c3 (two terms, second two rows) c5 = _mm256_add_ps(c5, c7); // c5 = c5 + c7 (the other two terms, second two rose) // Finally complete addition of all four terms and return the results mResult.n[0] = _mm256_add_ps(c0, c4); // n0 = a00*b00+a01*b10+a02*b20+a03*b30 a00*b01+a01*b11+a02*b21+a03*b31 a00*b02+a01*b12+a02*b22+a03*b32 a00*b03+a01*b13+a02*b23+a03*b33 // a10*b00+a11*b10+a12*b20+a13*b30 a10*b01+a11*b11+a12*b21+a13*b31 a10*b02+a11*b12+a12*b22+a13*b32 a10*b03+a11*b13+a12*b23+a13*b33 mResult.n[1] = _mm256_add_ps(c1, c5); // n1 = a20*b00+a21*b10+a22*b20+a23*b30 a20*b01+a21*b11+a22*b21+a23*b31 a20*b02+a21*b12+a22*b22+a23*b32 a20*b03+a21*b13+a22*b23+a23*b33 // a30*b00+a31*b10+a32*b20+a33*b30 a30*b01+a31*b11+a32*b21+a33*b31 a30*b02+a31*b12+a32*b22+a33*b32 a30*b03+a31*b13+a32*b23+a33*b33 return mResult; } 

Évidemment, vous pouvez extraire des termes de quatre masortingces à la fois et multiplier simultanément quatre masortingces en utilisant le même algorithme.