<-Précédent | Retour à l'accueil | Contact : etienne"at"les-sauvages.fr |
Afin de pouvoir faire des calculs plus compliqués que les simples opérations arithmétiques sur des vecteurs, et surtout pour travailler avec les matrices, nous avons besoin d'une opération qui s'appelle le produit matriciel. Pour vous la faire courte, il s'agit de calculer l'ensemble des produits scalaires possibles entre les lignes d'une matrice A et les colonnes d'une matrice B.
Simple, me direz-vous. Reprenons le code du produit scalaire. Hélas il faudrait transposer la matrice B pour transformer ses colonnes en lignes, et ensuite on perdrait l'avantage de la contiguité de la mémoire.
Néanmoins vous avez tout à fait raison. En C++ des familles, l'opération s'écrit :
void crossProduct(int m, int n, int p, double* A, double* B, double* C) { for (auto i = 0; i < m; i++) for (auto j = 0; j < p; j++) { C[i * p + j] = 0; for (auto k = 0; k < n; k++) { C[i * p + j] += A[i * n + k] * B[k * p + j]; } } }
Hélas en assembleur, la procédure va être plus complexe.
Je rappelle qu'on travaille avec MASM de Visual Studio sous Windows, sur architecture 64 bits, avec un processeur supportant AVX2 et FMA. On se relaxe, on ouvre ses chakras.
Oui. On va s'ouvrir le champ des possibles en calcul matriciel, à nous les décompositions PLU, les vecteurs propres et autres compositions matricielles. L'éclate totale.
Regardez bien le code C montré plus haut. La fonction prend 6 arguments. Or, Microsoft a décidé que le nombre d'arguments qu'on peut passer à une fonction par les registres est de 4. Pas un de plus, moins si l'on veut. Or nous, nous en avons besoin de 6. Enfer et damnation, que va-t-il advenir de nos arguments supplémentaires ?
Hé bien, oyez bien braves gens, ils vont être passés sur la pile.
La pile ? Késaco ? La pile, mes bons amis, est un espace de stockage de toutes sortes de valeurs, orienté dans le sens des adresses décroissantes. À l'envers, quoi. Plus on met de choses sur la pile, et plus son adresse sera petite. Un registre général lui est consacré : RSP (SP comme Stack Pointer). Et jouer avec la pile, c'est un sport très couru des compilateurs et développeurs assembleurs.
Ceci étant dit, qu'advient-il de nos paramètres à nous ? Accrochez-vous c'est tordu : les 4 premiers sont passés par les registres RCX, RDX, R8 et R9, et les autres sont placés sur la pile. Mais attention, ça pourrait être trop simple. Avant de mettre les paramètres supplémentaires sur la pile, la fonction appelante doit réserver sur la pile un espace dans lequel on puisse stocker les 4 premiers paramètres en considérant qu'il s'agit de registres : on appelle ça le shadow store. On trouvera donc notre paramètre 5 à RSP + ((4 + 1) * 8) + 8 = 48 (on commence par stocker RBP), et le sixième à RSP + 56.
On se repose, pour les calculs, sur cette instruction : VFMADD231PD. Elle travaille notamment sur des registres YMM, et effectue la multiplication de ses opérandes 2 et 3, qu'elle ajoute et stocke dans le premier opérande.
PUBLIC crossProduct_asm crossProduct_asm PROC push rbp mov rbp, rsp push r12 push r13 push r14 mov [rbp+16], ecx ;m ; mov [rbp+24], edx ;n mov [rbp+32], r8d ;p mov [rbp+40], r9 ;&A mov r10, [rbp+48] ;&B mov r14, r10 ;r14 <- &B mov r11, [rbp+56] ;&C mov r12, r11 ;r12<-&C mov rax, r8 ;rax <- p shr rax, 2 ; p/4->nombre de paquets de colonnes de B à traiter shl r8, 3 ;Taille en octets d'une ligne de B : 8 * p mov r13, rdx ;r13 <- n shr rdx, 2 ; n/4->nombre de paquets de colonnes de A à traiter shl r13, 3 ;Taille en octets d'une ligne de A : 8 * n boucle: vmovapd ymm0, YMMWORD PTR [r11] ;On charge le contenu de C (précédemment calculé) vmovapd ymm2, YMMWORD PTR [r9] ;On charge le contenu de A vpermpd ymm1, ymm2, 00000000b ;On prend le premier nombre dans A vfmadd231pd ymm0, ymm1, YMMWORD PTR [r10] ;On multiplie et additionne add r10, r8 ;On prend la ligne suivante de B vpermpd ymm1, ymm2, 01010101b ;On prend le second nombre dans A vfmadd231pd ymm0, ymm1, YMMWORD PTR [r10] ;On multiplie et additionne add r10, r8 ;On prend la ligne suivante de B vpermpd ymm1, ymm2, 10101010b ;On prend le troisième nombre dans A vfmadd231pd ymm0, ymm1, YMMWORD PTR [r10] ;On multiplie et additionne add r10, r8 ;On prend la ligne suivante de B vpermpd ymm1, ymm2, 11111111b ;On prend le quatrième nombre dans A vfmadd231pd ymm0, ymm1, YMMWORD PTR [r10] ;On multiplie et additionne vmovntpd YMMWORD PTR[r11], ymm0 ;On stocke C add r11, r8 ;On prend la ligne suivante de C add r9, r13 ;On prend la ligne suivante dans A mov r10, r14 ;On réinitialise la position dans B loop boucle ;On boucle sur m, le nombre de lignes de A ;reinit r9 mov r9, [rbp+40] ;On remet &A à sa valeur initiale mov r10, r14 add r10, 32 ; on ajoute 4 colonnes à B mov r14, r10 ; On stocke ces nouvelles 4 colonnes mov r11, r12 ;On remet &C à sa valeur initiale add r11, 32 ;On lui ajoute 4 colonnes mov r12, r11 ;Qu'on stocke mov ecx, [rbp+16] ;On réinitialise ecx à m dec eax ; on décrémmente p/4 cmp eax, 0 ja boucle ;On boucle sur p, le nombre de colonnes de B add r9, 32 ;On ajoute 4 colonnes à A mov [rbp+40], r9 mov r10, [rbp+48] ;On réinitialise B add r10, r8 add r10, r8 add r10, r8 add r10, r8 mov r14, r10 mov [rbp+48], r10 ;On réinitialise B mov r11, [rbp+56] ;On réinitialise C mov r12, r11 mov eax, [rbp+32] ;On réinitialise eax à p/4 shr eax, 2 dec rdx ;On passe aux colonnes de A suivantes. cmp rdx, 0 ja boucle pop r14 pop r13 pop r12 pop rbp ret crossProduct_asm ENDP
Attention, ce code ne fonctionne que sur des matrices alignées sur 32 octets, alors que malloc aligne sur du 16 octets. Il faut également que n et p soient des nombres divisibles par 4.
Le déterminant est un élément important en mathématiques. Il permet de savoir si des équations ont des solutions, ce genre de choses.
La méthode de calcul est longue et fastidieuse. Si longue et fastidieuse que pour les matrices de taille supérieure à, mettons, 10, on ne l'applique pas. On utilise d'autres techniques pour avoir un déterminant plus simple à calculer. Néanmoins, nous allons la coder comme exercice, car nous allons devoir faire appel à malloc() et free(), mais aussi à la récursivité.
La récursivité, c'est simplement quand une fonction s'appelle elle-même. La factorielle en est l'exemple typique : factorielle de 5 égale 5 fois factorielle de 4. En pseudo-code : fact(i) = i * fact(i-1). On met une condition d'arrêt, bien sûr. Ici, c'est si i == 1, fact(i) = 1;
PUBLIC lmatrice_asm lmatrice_asm PROC push rsi push rdi lea rsi, [r8 + rcx * 8] lea r8, [r9 + rdx * 8] dec rcx mov r10, rcx mov rax, rcx mul rcx mov rcx, rax mov rdi, r9 boucle: cmp rdi, r8 jne ecriture add rsi, 8 lea r8, [r8 + r10 * 8] ecriture: movsq loop boucle pop rdi pop rsi ret lmatrice_asm ENDP PUBLIC det_asm det_asm PROC ;double det_asm(int n, double mat[]) ;{ ; if (n == 1) ; { ; return mat[0]; ; } ; double resultat = 0.; ; int k = n - 1; ; double signe; ; signe = 1.; ; double* lmat = new double[k * k]; ; for (int i = 0; i < n; i++) ; { ; lmatrice_asm(n, i, mat, lmat); ; resultat = resultat + signe * mat[i] * det_asm(k, lmat); ; signe = -signe; ; } ; delete(lmat); ; return resultat; ;} cmp rcx, 1 jne continue vmovsd xmm0, QWORD PTR [rdx] ret continue: mov QWORD PTR [rsp+24], rbp ; sauvegarde de rbp dans la stack shadow push rdi push r14 push r15 sub rsp, 80 ; 00000050H ; On laisse 80 octets libres sur la pile. lea r15d, DWORD PTR [rcx-1] ; r15d <- k movaps XMMWORD PTR [rsp+64], xmm5 ; On stocke xmm5 en haut de la pile movsd xmm5, QWORD PTR __real@3ff0000000000000 mov edi, ecx ; edi <- n mov eax, r15d ; eax <- k mov rbp, rdx ; rdx <- l movaps XMMWORD PTR [rsp+48], xmm7 ; On stocke xmm7 plus bas dans la pile xorps xmm7, xmm7 ; xmm7 <- 0 lea r8, QWORD PTR [rax*8] ; r8 <- k * 8 mul r8 ; rax <- k * k * 8 mov rcx, rax ; rcx <- rax call malloc mov r14, rax ; r14 <- lmat mov QWORD PTR [rsp+112], rbx; on sauve RBX car il n'est pas volatile xor ebx, ebx ;RAZ de rbx mov QWORD PTR [rsp+120], rsi mov rsi, rbp movaps XMMWORD PTR [rsp+32], xmm4; on sauvegarde xmm4 movsd xmm4, QWORD PTR __xmm@8000000000000000 ; nop boucle: mov r9, r14 ; lmat mov r8, rbp ; mat mov edx, ebx ; l mov ecx, edi ; k call lmatrice_asm mov rdx, r14 ;lmat mov ecx, r15d ;k call det_asm ; det_asm movaps xmm1, xmm5 ;On met le signe dans xmm1 inc ebx mulsd xmm1, QWORD PTR [rsi]; Qu'on multiplie par mat[i] xorps xmm5, xmm4 add rsi, 8 ; vfmadd231pd xmm7, xmm0, xmm1 mulsd xmm0, xmm1 addsd xmm7, xmm0 cmp ebx, edi jl SHORT boucle movaps xmm4, XMMWORD PTR [rsp+32] mov rsi, QWORD PTR [rsp+120] mov rcx, r14 call free movsd xmm0, xmm7 mov rbp, QWORD PTR [rsp+128] mov rbx, QWORD PTR [rsp+112] movaps xmm5, XMMWORD PTR [rsp+64] movaps xmm7, XMMWORD PTR [rsp+48] add rsp, 80 ; 00000050H On détruit la pile pop r15 pop r14 pop rdi ret det_asm ENDP
J'obtiens, principalement pour de belles matrices, un temps d'exécution qui peut être divisé par deux pour le produit matriciel. La différence est moins flagrante pour le déterminant.