<-Précédent Retour à l'accueil Contact : etienne"at"les-sauvages.fr
  1. De quoi avons-nous besoin ?
  2. Est-ce qu'à part frimer devant la machine à café, ça a une utilité ?
  3. Mais pourquoi, comment ?
  4. De l'instruction VFMADD231PD
  5. Du code du produit matriciel.
  6. Du déterminant.
  7. De la récursivité.
  8. Du code.
  9. Résultats

    Assembleur : Calcul du produit matriciel, le come-back.

    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.

  1. De quoi avons-nous besoin ?
  2. 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.

  3. Est-ce qu'à part frimer devant la machine à café, ça a une utilité ?
  4. 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.

  5. La convention d'appel Windows 64 bits.
  6. 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.

  7. De l'instruction VFMADD231PD
  8. 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.

  9. Du code du produit matriciel
  10. 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.

  11. Du déterminant.
  12. 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é.

  13. De la récursivité.
  14. 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;

  15. Du code.
  16. 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

  17. Résultats.
  18. 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.