Verglichen wird der erzeugte Assembler Code der internen Fortran matmul() Routine mit einer von Hand geschriebenen Matrix Multiplikation.
Getestet wird für 3x3 Matrizen mit zur Compilezeit bekannter Größe. Sonst könnte man den vom FORTRAN Compiler erzeugten Assembler Code gar nicht mehr nachvollziehen.
subroutine intrinsicmatmul(a,b,c) implicit none real(8), intent(in) :: a(3,3), b(3,3) real(8), intent(inout) :: c(3,3) c = matmul(a,b) end subroutine
Und hier von Hand
subroutine handmatmul(a,b,c) implicit none real(8), intent(in) :: a(3,3), b(3,3) real(8), intent(inout) :: c(3,3) integer :: i,j,k c(:,:) = 0 do i=1, 3 do j=1, 3 do k=1, 3 c(j,i) = c(j,i) + a(j,k) * b(k,i) end do end do end do end subroutine
Getestet wird mit gfortran 7.3.0 -O1. Die Hand Version hat 13 Instruktionen mehr. Hiervon sind 6 Instruktionen für das Sichern und Wiederherstellen von Register. Der Rest geht wohl auf eine andere Art Adressberechnung drauf. Da die intrinsic matmul Routine selbst bei -O1 geinlint wird, ich sehe kein Funktionsaufruf im Assembler Code, wird sie wohl schneller sein.
Tests mit Praxiscode zeigen aber, dass das nicht so sein muss. Es wurden mehrere Matrizen multipliziert.
Es gilt: messen, messen messen. Bei dem nächsten Compiler/CPU kann es wieder anders sein.
intrinsic
intrinsicmatmul_: movq $0, (%rdx) movq $0, 8(%rdx) movq $0, 16(%rdx) movq $0, 24(%rdx) movq $0, 32(%rdx) movq $0, 40(%rdx) movq $0, 48(%rdx) movq $0, 56(%rdx) movq $0, 64(%rdx) leaq 24(%rdx), %rcx addq $24, %rsi leaq 96(%rdx), %r9 leaq 96(%rdi), %r10 .L4: leaq 24(%rdi), %rdx movq %rsi, %r8 .L3: movsd -24(%r8), %xmm1 movl $0, %eax .L2: addq $1, %rax movapd %xmm1, %xmm0 mulsd -32(%rdx,%rax,8), %xmm0 addsd -32(%rcx,%rax,8), %xmm0 movsd %xmm0, -32(%rcx,%rax,8) cmpq $3, %rax jne .L2 addq $8, %r8 addq $24, %rdx cmpq %r10, %rdx jne .L3 addq $24, %rcx addq $24, %rsi cmpq %r9, %rcx jne .L4 rep ret |
Hand
handmatmul_: pushq %r12 # - pushq %rbp # |- Register r12, rbp, rbx für Laufvariablen i,j,k freimachen pushq %rbx # - movq $0, (%rdx) # - movq $0, 8(%rdx) # | movq $0, 16(%rdx) # | movq $0, 24(%rdx) # | movq $0, 32(%rdx) # |- c(:,:) = 0 movq $0, 40(%rdx) # | movq $0, 48(%rdx) # | movq $0, 56(%rdx) # | movq $0, 64(%rdx) # - leaq 24(%rsi), %r9 leaq 96(%rsi), %r12 movq %rdx, %rcx subq %rsi, %rcx leaq -24(%rcx), %rbp movq %rcx, %rbx .L4: # i Schleife Anfang leaq 0(%rbp,%r9), %r8 movq %rdi, %r10 leaq (%rbx,%r9), %rdx .L3: # j Schleife Anfang movq %r8, %r11 movsd (%r8), %xmm1 # c(i,j) nach xmm1 laden movq %rsi, %rax movq %r10, %rcx .L2: # k Schleife Anfang movsd (%rcx), %xmm0 # - mulsd (%rax), %xmm0 # |- c(j,i) + a(j,k) * b(k,i) addsd %xmm0, %xmm1 # - addq $24, %rcx # 3x double = 24Byte addq $8, %rax # 1x double = 8byte. cmpq %r9, %rax # rax wird gleichzeitig als Pointer und Laufvariable benutzt. # In r9 steht der Wert den rax erreicht, wenn die Schleife terminiert jne .L2 # k Schleife Ende movsd %xmm1, (%r11) # c(j,i) = xmm1 addq $8, %r8 addq $8, %r10 cmpq %rdx, %r8 jne .L3 # j Schleife Ende addq $24, %rsi addq $24, %r9 cmpq %r12, %r9 jne .L4 # i Schleife Ende popq %rbx # - popq %rbp # |- Register aus dem Stack wieder herstellen popq %r12 # - ret |
Für Optimierung O2 gilt 35 Instruktionen für intrinsic und 43 für Hand.