Content

GEMM Micro Kernel für AVX

Für spezielle Werte von GEMM_MR und DGEMM_NR soll ein in Assembler geschriebener GEMM-Micro-Kernel verwendet werden.

Register bei der Intel64-Architektur

Die Assembler-Syntax ist relativ ähnlich zur ULM-Syntax (was kein Zufall ist). Dazu später mehr.

Parameterübergabe bei Funktionsaufrufen

Uns interessiert insbesondere wie Integer-Werte und Fließkommazahlen bei einem Funktionsaufruf übergeben werden:

Im Fall des GEMM-Micro-Kernel liegen die Argumente somit in folgenden Registern:

Parameter

Register

k

rdi

alpha

xmm0

A

rsi

B

rdx

beta

xmm1

C

rcx

incRowC

r8

incColC

r9

Konventionen zur Registerverwendung

Register gehören teilweise dem Caller (Aufrufer einer Funktion) oder Callee (aufgerufene Funktion). Soll in der Funktion ein Caller-Register verwendet werden muss der Inhalt zunächst gesichert werden und vor dem Rücksprung wiederhergestellt werden. Beispielsweise indem der Inhalt auf dem Stack gesichert wird.

Für uns relevant sind folgende Register:

rax

Callee

rbx

Caller

rcx

Callee

rdx

Callee

rsp

Caller

rbp

Caller

rsi

Callee

rdi

Callee

r8-r11

Callee

r12-r15

Caller

xmm0-xmm15

Callee

Rückkehr vom Funktionsaufruf

Die Rücksprungadresse wird über den Stack übergeben (und liegt auf der Stack-Spitze). Mit dem Befehl retq wird der Stack abgebaut und zur Rücksprungsadresse zurückgekehrt.

Rumpf für den GEMM-Micro-Kernel

#  void
#  gemm_micro(long   k,
#             double alpha,
#             double *A,
#             double *B,
#             double beta,
#             double *C, long incRowC, long incColC);
.globl ugemm_4_8
ugemm_4_8:

#  Arguments:
#           %rdi     long           k
#           %xmm0    double         alpha
#           %rsi     double         *A
#           %rdx     double         *B
#           %xmm1    double         beta
#           %rcx     double         *C
#           %r8      long           incRowC
#           %r9      long           incColC


#
#       ... code for function ...
#


        retq

Befehle für Integer-Register

Im Gegensatz zur ULM haben Befehle für Integer-Register auf der Intel64-Architektur meist nur zwei Argumente oder sogar nur ein Argument.

Laden und Speichern von Daten

movq     (%rsi),     %r10

64-Bit von Adresse %rsi in Register r10 laden

movq    8(%rsi),     %r10

64-Bit von Adresse %rsi+8 in Register r10 laden

movq    (%rcx,%r8),  %r10

64-Bit von Adresse %rcx+%r8 in Register r10 laden

movq     %r10,       (%rsi)

64-Bit Inhalt von Register r10 an die Adresse %rsi laden.

movq     %r10,      8(%rsi)

64-Bit Inhalt von Register r10 an die Adresse %rsi+8 laden.

movq     %r10,       (%rsi,%r8)

64-Bit Inhalt von Register r10 an die Adresse %rsi+%r8 laden.

Bei der Angabe einer Adresse wird allgemein das Konstrukt d(a, b, c) interpretiert als der Speicherinhalt bei Adresse \(\text{%}a + c\cdot\text{%}b + d\). Dabei sind für \(c\) allerdings nur die Wert \(1\), \(2\) oder \(4\) erlaubt. Mit dem Befehl leaq (Load effective Adresse) kann dies beispielsweise ausgenutzt werden, um eine Integer-Zahl mit 2 oder 4 zu multiplizieren:

    leaq (,%r8,2)   %r10

Hier wird (,%r8,2) als Adresse \(2\cdot \text{%r8}\) interpretiert und dieser Wert in das Register r10 kopiert.

Integer-Arithmetik

addq     %rsi,       %rdi

\(\text{%rsi} + \text{%rdi} \rightarrow \text{%rdi}\)

addq     $10,        %rdi

\(10 + \text{%rdi} \rightarrow \text{%rdi}\)

subq     %rsi,       %rdi

\(\text{%rdi} - \text{%rsi} \rightarrow \text{%rdi}\)

subq     $10,        %rdi

\(\text{%rdi} - 10 \rightarrow \text{%rdi}\)

incq     %rdi

\(\text{%rdi} + 1 \rightarrow \text{%rdi}\)

decq     %rdi

\(\text{%rdi} - 1 \rightarrow \text{%rdi}\)

Mit testq wird eine bitweise Und-Verknüpfung ausgeführt, die nur die Statusregister setzt aber kein Register-Argumnet verändert:

testq    %rsi,       %rdi

\(\text{%rdi} \land \text{%rsi}\)

testq    $10,        %rdi

\(\text{%rdi} \land 10\)

Insbesondere kann mit testq \text{%}rdi, \text{%}rdi überprüft werden, ob ein Register den Wert Null enthält oder nicht.

Sprungbefehle

Wie beim ULM-Assembler können Befehle mit einem Label markiert werden und bei Sprungbefehlen als Ziel angegeben werden.

jmp      foo

Unbedingter Sprung zum Label foo

jl       foo

Bedingter jump less Sprung zum Label foo

Die Bezeichnung aller weiteren Varianten (je, jg, ...) von bedingten Sprüngen entsprechen denen des ULM-Assembler.

Befehle für AVX-Register

Beachtet, dass zum Beispiel mit %xmm0 und %ymm0 dasselber Register bezeichnet wird. Mit %xmm0 werden davon aber nur die unteren 128-Bit angesprochen. Enthält ymm0 etwa die vier double-Werte \(a\), \(b\), \(c\) und \(d\). Entspricht das dem Bit-Muster \((d,c,b,a)_{64}\). Der Inhalt von %xmm0 ist dann \((b,a)_{64}\). Wobei \(a\) und \(b\) in beiden Fällen identisch sind.

Laden von Daten

vmovapd        (%rsi),     %ymm0

move aligned packed double: Von Adresse %rsi 256-Bit in Register ymm0 laden. Die Adresse %rsi muss durch 32 teilbar sein.

vbroadcastsd   (%rsi),     %ymm0

broadcast single double: Von Adresse %rsi 64-Bit in die vier Slots des Registers ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Werte \((a,a,a,a)_{64}\).

vmovlpd        (%rsi), %ymm0, %ymm0

move low packed double: Von Adresse %rsi 64-Bit in Register in die untersten 64-Bit von ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Wert \((*,*,*,a)_{64}\). Mit Stern markierte Werte werden nicht verändert.

vmovhpd        (%rsi), %ymm0, %ymm0

move high packed double: Von Adresse %rsi 64-Bit in Register in die nächst höheren 64-Bit von ymm0 laden. Ist \(a\) der Wert bei Adresse %rsi, dann enthält %ymm0danach die Wert \((*,*,a,*)_{64}\).

Speichern von Daten

vmovapd        %ymm0,      (%rsi)

move aligned packed double: Nach Adresse %rsi 256-Bit aus Register ymm0 laden. Die Adresse %rsi muss durch 32 teilbar sein.

vmovlpd        %ymm0,      (%rsi)

move low packed double: Nach Adresse %rsi die untersten 64-Bit aus Register laden.

vmovhpd        %ymm0,      (%rsi)

move high packed double: Nach Adresse %rsi das zweit-unterste 64-Bitmuster von Register %ymm0 laden.

Sowohl beim Laden als auch Speichern sind die üblichen Varianten zur Angabe einer Adresse möglich:

Muliplikation und Addition

vmulpd        %ymm0,  %ymm1, %ymm2

multiplicate packed double: Komponentenweise Multiplikation \((a_3,a_2,a_1,a_0)\otimes (b_3,b_2,b_1,b_0)\rightarrow(a_3\cdot b_3,a_2\cdot b_2,a_1\cdot b_1,a_0\cdot b_0)\)

vaddpd        %ymm0,  %ymm1, %ymm2

multiplicate packed double: Komponentenweise Addition \((a_3,a_2,a_1,a_0) \oplus (b_3,b_2,b_1,b_0) \rightarrow (a_3+b_3,a_2+b_2,a_1+b_1,a_0+b_0)\)

Bit-Operationen

Wir benötigen nur eine XOR-Operation, um ein Register auf Null zusetzen. Allgemein lautet der Befehl

vxorpd        %ymm0,  %ymm1, %ymm2

xor packed double: Komponentenweise XOR-Verknüpfung \((a_3,a_2,a_1,a_0)\veebar (b_3,b_2,b_1,b_0)\rightarrow(a_3\veebar b_3,a_2\veebar b_2,a_1\veebar b_1,a_0 \veebar b_0)\)

AVX-Algorithmus für GEMM-Micro Kernel

Als Paramater für die Computer im E44 wählen wir \(M_r = 4\) und \(N_r = 8\). Damit ergibt sich die spezielle Operation

\[C\leftarrow\beta\;\underbrace{\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)}_{=C}+\alpha\;\left(\begin{array}{cccc} a_{0,0} & a_{0,1} & \dots & a_{0,k-1} \\ a_{1,0} & a_{1,1} & \dots & a_{1,k-1} \\ a_{2,0} & a_{2,1} & \dots & a_{2,k-1} \\ a_{3,0} & a_{3,1} & \dots & a_{3,k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0,0} & b_{0,1} & b_{0,2} & b_{0,3} & b_{0,4} & b_{0,5} & b_{0,6} & b_{0,7} \\ b_{1,0} & b_{1,1} & b_{1,2} & b_{1,3} & b_{1,4} & b_{1,5} & b_{1,6} & b_{1,7} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ b_{k-1,0} & b_{k-1,1} & b_{k-1,2} & b_{k-1,3} & b_{k-1,4} & b_{k-1,5} & b_{k-1,6} & b_{k-1,7} \\ \end{array}\right)\]

Die Elemente \(a_{i,\ell}\) und \(b_{\ell,j}\) mit \(0\leq i < 4\), \(0\leq j < 8\) und \(0\leq \ell < k\) nummerieren wir entsprechend der Reihenfolge in der diese im Speicher liegen:

\[C\leftarrow\beta\;\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)+\alpha\;\left(\begin{array}{cccc} a_{0} & a_{4} & \dots & a_{4\cdot k-4} \\ a_{1} & a_{5} & \dots & a_{4\cdot k-3} \\ a_{2} & a_{6} & \dots & a_{4\cdot k-2} \\ a_{3} & a_{7} & \dots & a_{4\cdot k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ b_{8\cdot k-8} & b_{8\cdot k-7} & b_{8\cdot k-6} & b_{8\cdot k-5} & b_{8\cdot k-4} & b_{8\cdot k-3} & b_{8\cdot k-2} & b_{8\cdot k-1} \\ \end{array}\right)\]

Das Produkt der Paneelen betrachten wir als Summe dyadischer Produkte:

\[C\leftarrow\beta\;\left(\begin{array}{cccccccc} c_{0,0} & c_{0,1} & c_{0,2} & c_{0,3} & c_{0,4} & c_{0,5} & c_{0,6} & c_{0,7} \\ c_{1,0} & c_{1,1} & c_{1,2} & c_{1,3} & c_{1,4} & c_{1,5} & c_{1,6} & c_{1,7} \\ c_{2,0} & c_{2,1} & c_{2,2} & c_{2,3} & c_{2,4} & c_{2,5} & c_{2,6} & c_{2,7} \\ c_{3,0} & c_{3,1} & c_{3,2} & c_{3,3} & c_{3,4} & c_{3,5} & c_{3,6} & c_{3,7} \\ \end{array}\right)+\alpha\;\left(\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right)+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right)+\dots+\left(\begin{array}{c} a_{4\cdot k-4} \\ a_{4\cdot k-3} \\ a_{4\cdot k-2} \\ a_{4\cdot k-1} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8\cdot k-8} & b_{8\cdot k-7} & b_{8\cdot k-6} & b_{8\cdot k-5} & b_{8\cdot k-4} & b_{8\cdot k-3} & b_{8\cdot k-2} & b_{8\cdot k-1} \\ \end{array}\right)\right)\]

Zuordnung von Registern

Die Werte von \(\alpha\) und \(\beta\) in den Registern ymm6 und ymm7 werden zunächst nicht benötigt. Um die Register anderweitig benutzen zu können retten wir deren Inhalt auf den Stack:

        vmovlpd                 %xmm0,       -8(%rsp)
        vmovlpd                 %xmm1,      -16(%rsp)

Um ebenfalls die Register rax und rbx verwenden zu können retten wir auch diese:

        movq                    %rax,       -24(%rsp)
        movq                    %rbx,       -32(%rsp)

Die Register ymm8 bis ymm15 benutzen wir, um dis Summe der dyadischen Produkte zu puffern:

\[\begin{array}{lcllcl}\text{%ymm8 } & \leadsto & \left(c_{0,0},\,c_{0,1},\,c_{0,2},\,c_{0,3}\right), & \qquad\text{%ymm9 } & \leadsto & \left(c_{0,4},\,c_{0,5},\,c_{0,6},\,c_{0,7}\right), \\\text{%ymm10} & \leadsto & \left(c_{1,0},\,c_{1,1},\,c_{1,2},\,c_{1,3}\right), & \qquad\text{%ymm11} & \leadsto & \left(c_{1,4},\,c_{1,5},\,c_{1,6},\,c_{1,7}\right), \\\text{%ymm12} & \leadsto & \left(c_{2,0},\,c_{2,1},\,c_{2,2},\,c_{2,3}\right), & \qquad\text{%ymm13} & \leadsto & \left(c_{2,4},\,c_{2,5},\,c_{2,6},\,c_{2,7}\right), \\\text{%ymm14} & \leadsto & \left(c_{3,0},\,c_{3,1},\,c_{3,2},\,c_{3,3}\right), & \qquad\text{%ymm15} & \leadsto & \left(c_{3,4},\,c_{3,5},\,c_{3,6},\,c_{3,7}\right). \\\end{array}\]

Diese Register werden zunächst mit Null initialisiert.

Die Register ymm0 bis ymm3 verwenden wir um mit einem Broadcast einen Spaltenvektor der Paneele \(A\) zu laden. Im ersten Schritt (\(\ell=0\)) ergibt sich:

\[\begin{array}{lcl}\text{%ymm0 } & \leftarrow & \left(a_{0},\,a_{0},\,a_{0},\,a_{0}\right) \\\text{%ymm1 } & \leftarrow & \left(a_{1},\,a_{1},\,a_{1},\,a_{1}\right) \\\text{%ymm2 } & \leftarrow & \left(a_{2},\,a_{2},\,a_{2},\,a_{2}\right) \\\text{%ymm3 } & \leftarrow & \left(a_{3},\,a_{3},\,a_{3},\,a_{3}\right) \\\end{array}\]

In ymm4 und ymm5 laden wir eine Zeile aus der Paneele von \(B\). Im Fall \(\ell=0\) also

\[\begin{array}{lcllcl}\text{%ymm4 } & \leftarrow & \left(b_{0},\,b_{1},\,b_{2},\,b_{3}\right), & \qquad\text{%ymm5 } & \leadsto & \left(b_{4},\,b_{5},\,b_{6},\,b_{7}\right), \\\end{array}\]

Durch eine komponentenweise Multiplikation kann somit das dyadische Produkt berechnet werden:

\[\begin{array}{lcllcl}\text{%ymm0 }\otimes\text{%ymm4 } & = & \left(a_{0}b_{0},\,a_{0}b_{1},\,a_{0}b_{2},\,a_{0}b_{3}\right) & \text{%ymm0 }\otimes\text{%ymm5 } & = & \left(a_{0}b_{4},\,a_{0}b_{1},\,a_{0}b_{2},\,a_{0}b_{3}\right) \\\text{%ymm1 }\otimes\text{%ymm4 } & = & \left(a_{1}b_{0},\,a_{1}b_{1},\,a_{1}b_{2},\,a_{1}b_{3}\right) & \text{%ymm1 }\otimes\text{%ymm5 } & = & \left(a_{1}b_{4},\,a_{1}b_{1},\,a_{1}b_{2},\,a_{1}b_{3}\right) \\\text{%ymm2 }\otimes\text{%ymm4 } & = & \left(a_{2}b_{0},\,a_{2}b_{1},\,a_{2}b_{2},\,a_{2}b_{3}\right) & \text{%ymm2 }\otimes\text{%ymm5 } & = & \left(a_{2}b_{4},\,a_{2}b_{1},\,a_{2}b_{2},\,a_{2}b_{3}\right) \\\text{%ymm3 }\otimes\text{%ymm4 } & = & \left(a_{3}b_{0},\,a_{3}b_{1},\,a_{3}b_{2},\,a_{3}b_{3}\right) & \text{%ymm3 }\otimes\text{%ymm5 } & = & \left(a_{3}b_{4},\,a_{3}b_{1},\,a_{3}b_{2},\,a_{3}b_{3}\right) \\\end{array}\]

Die noch freien Register ymm6 und ymm7 werden für Zwischenergebnisse verwendet. Bezeichne \(\overline{AB}\) den Puffer für die Summe der dyadischen Produkte. Die Berechnung von

\[\overline{AB}\leftarrow\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right)+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right)+\dots\]

Realisieren wir iterativ durch

\[\overline{AB}\leftarrow\mathbf{0},\quad\overline{AB}\leftarrow\overline{AB}+\left(\begin{array}{c} a_{0} \\ a_{1} \\ a_{2} \\ a_{3} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{0} & b_{1} & b_{2} & b_{3} & b_{4} & b_{5} & b_{6} & b_{7} \\ \end{array}\right),\quad\overline{AB}\leftarrow\overline{AB}+\left(\begin{array}{c} a_{4} \\ a_{5} \\ a_{6} \\ a_{7} \\ \end{array}\right)\left(\begin{array}{cccccccc} b_{8} & b_{9} & b_{10} & b_{11} & b_{12} & b_{13} & b_{14} & b_{15} \\ \end{array}\right),\;\dots\]

Als Pseudo-Code für die AVX-Architektur erhalten wir:

Skalierung und Aufdatierung

Anschließend wird der Puffer skaliert

\[\overline{AB} \leftarrow \alpha \overline{AB}\]

und die Matrix \(C\) aufdatiert

\[C \leftarrow \beta C + \overline{AB}\]

Skalierung

Die Skalierung des Puffers erfolgt durch

Der Wert für \(\alpha\) muss dazu zunächst vom Stack geladen werd:

        vbroadcastsd         -8(%rsp),         %ymm6

Aufdatierung

Die Aufdatierung von \(C\) erfolgt zeilenweise. Zu beachten ist, dass wir keine zeilen- oder spaltenweise Speicherung von \(C\) voraussetzen. Jedes Matrixelement muss also einzeln geladen und zurückgeschrieben werden. In C erfolgt der Zugriff auf die erts Zeile mit C[0*incColC], C[1*incColC], ..., C[7*incColC].

In unserem Fall ist der Wert incColC im Register r9. Zu beachten ist, dass in C bei der Adressierung implizit der Faktor sizeof(C) auf einen Index angewandt wird. Im Assembler-Code muss dies explizit erfolgen. Wir multiplizieren deshlab zunächst incRowC und incColC mit 8:

        leaq                    (,%r8,8),       %r8
        leaq                    (,%r9,8),       %r9     # 1*incCol

In die Register r10, r11 und rdx schreiben wir zusätzlich Werte, um einfacher auf weiteren Spalten zugreifen zu können:

        leaq                    (,%r9,2),       %r10     # 2*incCol
        leaq                    (%r10,%r9),     %r11     # 3*incCol
        leaq                    (%rcx,%r10,2),  %rdx     # 4*incCol

In das Register ymm7 laden wir den Wert für \(\beta\) wieder vom Stack:

        vbroadcastsd         -16(%rsp),         %ymm7

Mit dem Befehl

        vextractf128            $1,             %ymm8,          %xmm4

werden die oberen 128-Bit von Register ymm8 in die unteren 128-Bit von Register ymm4 geladen. Die unteren 128-Bit von ymm4 sind identisch mit xmm4.

Die erste Zeile von \(C\) kann dann wie folgt aufdatiert werden:

#
#       Update C(0,:)
#
        vmovlpd                 (%rcx),         %xmm0,          %xmm0
        vmovhpd                 (%rcx,%r9),     %xmm0,          %xmm0
        vmovlpd                 (%rcx,%r10),    %xmm1,          %xmm1
        vmovhpd                 (%rcx,%r11),    %xmm1,          %xmm1
        vmovlpd                 (%rdx),         %xmm2,          %xmm2
        vmovhpd                 (%rdx,%r9),     %xmm2,          %xmm2
        vmovlpd                 (%rdx,%r10),    %xmm3,          %xmm3
        vmovhpd                 (%rdx,%r11),    %xmm3,          %xmm3

        vmulpd                  %xmm7,          %xmm0,          %xmm0
        vmulpd                  %xmm7,          %xmm1,          %xmm1
        vmulpd                  %xmm7,          %xmm2,          %xmm2
        vmulpd                  %xmm7,          %xmm3,          %xmm3

        vextractf128            $1,             %ymm8,          %xmm4
        vextractf128            $1,             %ymm9,          %xmm5

        vaddpd                  %xmm0,          %xmm8,          %xmm0
        vaddpd                  %xmm1,          %xmm4,          %xmm1
        vaddpd                  %xmm2,          %xmm9,          %xmm2
        vaddpd                  %xmm3,          %xmm5,          %xmm3

        vmovlpd                 %xmm0,          (%rcx)
        vmovhpd                 %xmm0,          (%rcx,%r9)
        vmovlpd                 %xmm1,          (%rcx,%r10)
        vmovhpd                 %xmm1,          (%rcx,%r11)
        vmovlpd                 %xmm2,          (%rdx)
        vmovhpd                 %xmm2,          (%rdx,%r9)
        vmovlpd                 %xmm3,          (%rdx,%r10)
        vmovhpd                 %xmm3,          (%rdx,%r11)

Vorlage

Schreibt in ugemm_4_8.s die obige Vorlage für die Funktion Funktion ugemm_4_8. Mit as -o ugemm_4_8.o ugemm_4_8.s kann daraus ein Object-File erzeugt werden.

Zum Testen kann folgendes C-Programm bench_dgemm_4_8.c benutzt werden. Mit gcc -Wall -O3 ugemm_4_8.o bench_dgemm_4_8.c kann ein ausführbaren Programm erzeugt werden.

#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <sys/times.h>
#include <unistd.h>
#include <stdint.h>

//-- setup and print matrices --------------------------------------------------
double
walltime()
{
   struct tms    ts;
   static double ClockTick=0.0;

   if (ClockTick==0.0) {
        ClockTick = 1.0 / ((double) sysconf(_SC_CLK_TCK));
   }
   return ((double) times(&ts)) * ClockTick;
}

void
initGeMatrix(int m, int n, double *A, int incRowA, int incColA)
{
    int i, j;

    for (i=0; i<m; ++i) {
        for (j=0; j<n; ++j) {
            A[i*incRowA+j*incColA] = i*n + j + 1;
        }
    }
}

void
randGeMatrix(int m, int n, double *A, int incRowA, int incColA)
{
    int i, j;
    for (j=0; j<n; ++j) {
        for (i=0; i<m; ++i) {
            A[i*incRowA+j*incColA] = ((double)rand()-RAND_MAX/2)*200/RAND_MAX;
        }
    }
}


void
printGeMatrix(int m, int n, const double *A, int incRowA, int incColA)
{
    int i, j;

    for (i=0; i<m; ++i) {
        for (j=0; j<n; ++j) {
            printf("%10.4lf ", A[i*incRowA+j*incColA]);
        }
        printf("\n");
    }
    printf("\n\n");
}

void
dgemm_ref(int m, int n, int k,
          double alpha,
          const double *A, int incRowA, int incColA,
          const double *B, int incRowB, int incColB,
          double beta,
          double *C, int incRowC, int incColC)
{
    int i, j, l;

    if (beta!=1) {
        for (i=0; i<m; ++i) {
            for (j=0; j<n; ++j) {
                if (beta!=0) {
                    C[i*incRowC+j*incColC] *= beta;
                } else {
                    C[i*incRowC+j*incColC] = 0;
                }
            }
        }
    }
    if (alpha!=0) {
        for (i=0; i<m; ++i) {
            for (j=0; j<n; ++j) {
                for (l=0; l<k; ++l) {
                    C[i*incRowC+j*incColC] += alpha*A[i*incRowA+l*incColA]
                                                   *B[l*incRowB+j*incColB];
                }
            }
        }
    }
}

double
dgenrm1(int m, int n, const double *A, int incRowA, int incColA)
{
    int     i, j;
    double  result = 0;

    for (j=0; j<n; ++j) {
        double sum = 0;
        for (i=0; i<m; ++i) {
            sum += fabs(A[i*incRowA+j*incColA]);
        }
        if (sum>result) {
            result = sum;
        }
    }
    return result;
}

void
dgecopy(int m, int n,
        const double *X, int incRowX, int incColX,
        double *Y, int incRowY, int incColY)
{
    int i, j;

    for (i=0; i<m; ++i) {
        for (j=0; j<n; ++j) {
            Y[i*incRowY+j*incColY] = X[i*incRowX+j*incColX];
        }
    }
}

void
dgescal(int m, int n, double alpha,
        double *X, int incRowX, int incColX)
{
    int i, j;

    for (j=0; j<n; ++j) {
        for (i=0; i<m; ++i) {
            X[i*incRowX+j*incColX] *= alpha;
        }
    }
}

void
dgeaxpy(int m, int n, double alpha,
        const double *X, int incRowX, int incColX,
        double *Y, int incRowY, int incColY)
{
    int i, j;

    if (alpha==0 || m==0 || n==0) {
        return;
    }
    for (j=0; j<n; ++j) {
        for (i=0; i<m; ++i) {
            Y[i*incRowY+j*incColY] += alpha*X[i*incRowX+j*incColX];
        }
    }
}

#define MIN(X,Y)   ((X)<(Y) ? (X) : (Y))
#define MAX(X,Y)   ((X)>(Y) ? (X) : (Y))

double
err_dgemm(int m, int n, int k,
          double alpha,
          const double *A, int incRowA, int incColA,
          const double *B, int incRowB, int incColB,
          double beta,
          const double *C0, int incRowC0, int incColC0,
          double *C, int incRowC, int incColC)
{
    double normA = dgenrm1(m, k, A, incRowA, incColA);
    double normB = dgenrm1(k, n, B, incRowB, incColB);
    double normC = dgenrm1(m, n, C, incRowC0, incColC0);
    double normD;
    int    mn    = (m>n)  ? m  : n;
    int    mnk   = (mn>k) ? mn : k;

    normA = MAX(normA, fabs(alpha)*normA);
    normC = MAX(normC, fabs(beta)*normC);

    dgeaxpy(m, n, -1.0, C0, incRowC0, incColC0, C, incRowC, incColC);
    normD = dgenrm1(m, n, C, incRowC, incColC);

    return normD/(mnk*normA*normB*normC);
}

//------------------------------------------------------------------------------
//-- new with alignment --------------------------------------------------------
void *
malloc_(size_t alignment, size_t size)
{
    size     += alignment;

    void *ptr  = malloc(size);
    void *ptr2 = (void *)(((size_t)ptr + alignment) & ~(alignment-1));
    void **vp  = (void**) ptr2 - 1;
    *vp        = ptr;
    return ptr2;
}

void
free_(void *ptr)
{
    free(*((void**)ptr-1));
}
//------------------------------------------------------------------------------

#define DGEMM_MC 256
#define DGEMM_NC 512
#define DGEMM_KC 256

#define DGEMM_MR 4
#define DGEMM_NR 8

//------------------------------------------------------------------------------

void
dpack_A(int m, int k, const double *A, int incRowA, int incColA, double *p)
{
    int i, i0, j, l, nu;
    int mp = (m+DGEMM_MR-1) / DGEMM_MR;

    for (j=0; j<k; ++j) {
        for (l=0; l<mp; ++l) {
            for (i0=0; i0<DGEMM_MR; ++i0) {
                i     = l*DGEMM_MR + i0;
                nu    = l*DGEMM_MR*k + j*DGEMM_MR + i0;
                p[nu] = (i<m) ? A[i*incRowA+j*incColA]
                              : 0;
            }
        }
    }
}

void
dpack_B(int k, int n, const double *B, int incRowB, int incColB, double *p)
{
    int i, j, j0, l, nu;
    int np = (n+DGEMM_NR-1) / DGEMM_NR;

    for (l=0; l<np; ++l) {
        for (j0=0; j0<DGEMM_NR; ++j0) {
            j = l*DGEMM_NR + j0;
            for (i=0; i<k; ++i) {
                nu    = l*DGEMM_NR*k + i*DGEMM_NR + j0;
                p[nu] = (j<n) ? B[i*incRowB+j*incColB]
                              : 0;
            }
        }
    }
}

void
ugemm_4_8(int64_t k,
          double alpha,
          const double *A,
          const double *B,
          double beta,
          double *C, int64_t incRowC, int64_t incColC);

void
dgemm_macro(int m, int n, int k,
            double alpha,
            const double *A,
            const double *B,
            double beta,
            double *C, int incRowC, int incColC)
{
    double C_[DGEMM_MR*DGEMM_NR];
    int i, j;

    const int MR = DGEMM_MR;
    const int NR = DGEMM_NR;

    for (j=0; j<n; j+=NR) {
        int nr = (j+NR<n) ? NR
                          : n - j;

        for (i=0; i<m; i+=MR) {
            int mr = (i+MR<m) ? MR
                              : m - i;

            if (mr==MR && nr==NR) {
                ugemm_4_8(k, alpha,
                          &A[i*k], &B[j*k],
                          beta,
                          &C[i*incRowC+j*incColC], incRowC, incColC);
            } else {
                ugemm_4_8(k, alpha,
                          &A[i*k], &B[j*k],
                          0.,
                          C_, 1, MR);
                dgescal(mr, nr, beta,
                        &C[i*incRowC+j*incColC], incRowC, incColC);
                dgeaxpy(mr, nr, 1., C_, 1, MR,
                        &C[i*incRowC+j*incColC], incRowC, incColC);
            }
        }
    }
}

void
dgemm(int m, int n, int k,
      double alpha,
      const double *A, int incRowA, int incColA,
      const double *B, int incRowB, int incColB,
      double beta,
      double *C, int incRowC, int incColC)
{
    int i, j, l;

    const int MC = DGEMM_MC;
    const int NC = DGEMM_NC;
    const int KC = DGEMM_KC;

    if (alpha==0.0 || k==0) {
        dgescal(m, n, beta, C, incRowC, incColC);
    } else {

        double *A_ = (double *)malloc_(32, sizeof(double)*DGEMM_MC*DGEMM_KC);
        double *B_ = (double *)malloc_(32, sizeof(double)*DGEMM_KC*DGEMM_NC);

        for (j=0; j<n; j+=NC) {
            int nc = (j+NC<=n) ? NC
                               : n - j;

            for (l=0; l<k; l+=KC) {
                int    kc    = (l+KC<=k) ? KC
                                         : k - l;
                double beta_ = (l==0) ? beta
                                      : 1;

                dpack_B(kc, nc,
                        &B[l*incRowB+j*incColB], incRowB, incColB,
                        B_);

                for (i=0; i<m; i+=MC) {
                    int mc = (i+MC<=m) ? MC
                                       : m - i;
                    dpack_A(mc, kc,
                            &A[i*incRowA+l*incColA], incRowA, incColA,
                            A_);

                    dgemm_macro(mc, nc, kc,
                                alpha,
                                A_, B_,
                                beta_,
                                &C[i*incRowC+j*incColC], incRowC, incColC);
                }
            }
        }

        free_(A_);
        free_(B_);
    }
}

//------------------------------------------------------------------------------
#ifndef ALPHA
#define ALPHA 2
#endif

#ifndef BETA
#define BETA 2
#endif

#ifndef MIN_N
#define MIN_N 100
#endif

#ifndef MAX_N
#define MAX_N 1500
#endif

#ifndef INC_N
#define INC_N 100
#endif

#ifndef MIN_M
#define MIN_M 100
#endif

#ifndef MAX_M
#define MAX_M 1500
#endif

#ifndef INC_M
#define INC_M 100
#endif

#ifndef MIN_K
#define MIN_K 100
#endif

#ifndef MAX_K
#define MAX_K 1500
#endif

#ifndef INC_K
#define INC_K 100
#endif

#ifndef ROWMAJOR_A
#define ROWMAJOR_A 0
#endif

#ifndef ROWMAJOR_B
#define ROWMAJOR_B 0
#endif

#ifndef ROWMAJOR_C
#define ROWMAJOR_C 0
#endif

#if (ROWMAJOR_A==1)
#   define INCROW_A  MAX_K
#   define INCCOL_A  1
#else
#   define INCROW_A  1
#   define INCCOL_A  MAX_M
#endif

#if (ROWMAJOR_B==1)
#   define INCROW_B  MAX_N
#   define INCCOL_B  1
#else
#   define INCROW_B  1
#   define INCCOL_B  MAX_K
#endif

#if (ROWMAJOR_C==1)
#   define INCROW_C  MAX_N
#   define INCCOL_C  1
#else
#   define INCROW_C  1
#   define INCCOL_C  MAX_M
#endif


double A_[MAX_M*MAX_K*MIN(INCROW_A,INCCOL_A)];
double B_[MAX_K*MAX_N*MIN(INCROW_B,INCCOL_B)];
double C_[MAX_M*MAX_N];

double C_0[MAX_M*MAX_N*MIN(INCROW_A,INCCOL_A)];
double C_1[MAX_M*MAX_N*MIN(INCROW_A,INCCOL_A)];

int
main()
{
    int m, n, k;

    randGeMatrix(MAX_M, MAX_K, A_, INCROW_A, INCCOL_A);
    randGeMatrix(MAX_K, MAX_N, B_, INCROW_B, INCCOL_B);
    randGeMatrix(MAX_M, MAX_N, C_, 1, MAX_M);

    printf("#%9s %9s %9s", "m", "n", "k");
    printf(" %12s %12s %12s", "t", "MFLOPS", "err");
    printf(" %12s %12s %12s", "t", "MFLOPS", "err");
    printf(" %12s %12s %12s", "t", "MFLOPS", "err");
    printf("\n");

    for (m=MIN_M, n=MIN_N, k=MIN_K;
         n<=MAX_N && m<=MAX_M && k<=MAX_K;
         m+=INC_M, n+=INC_N, k+=INC_K)
    {
        double t, dt, err;
        int    runs  = 1;
        double ops   = 2.0*m/1000*n/1000*k;

        printf(" %9d %9d %9d", m, n, k);

        // compute reference solution
        dgecopy(m, n, C_, 1, MAX_M, C_0, INCROW_C, INCCOL_C);
        dgemm_ref(m, n, k,
                  ALPHA,
                  A_, INCROW_A, INCCOL_A,
                  B_, INCROW_B, INCCOL_B,
                  BETA,
                  C_0, INCROW_C, INCCOL_C);

        // bench dgemm

        t    = 0;
        runs = 0;
        do {
            dgecopy(m, n, C_, 1, MAX_M, C_1, INCROW_C, INCCOL_C);
            dt = walltime();
            dgemm(m, n, k,
                  ALPHA,
                  A_, INCROW_A, INCCOL_A,
                  B_, INCROW_B, INCCOL_B,
                  BETA,
                  C_1, INCROW_C, INCCOL_C);
            dt = walltime() - dt;
            t += dt;
            ++runs;
        } while (t<0.3);
        t /= runs;

        err = err_dgemm(m, n, k,
                        ALPHA,
                        A_, INCROW_A, INCCOL_A,
                        B_, INCROW_B, INCCOL_B,
                        BETA,
                        C_0, INCROW_C, INCCOL_C,
                        C_1, INCROW_C, INCCOL_C);

        printf(" %12.2e %12.2lf %12.2e", t, ops/t, err);

        printf("\n");
    }

    return 0;
}