Ich bin immer noch bei WIRKLICH trivialen Funktionen und wollte mal was machen, das zu mehr als fünf Befehlen führt … OMFG, da geht alles schief.
Nehmen wir mal das hier:
__m128i multiply_r8g8b8a8(__m128i l, __m128i r)
Zwei Farben multiplizieren, die als 8-Bit RGBA vorliegen. Nichts ausgefallenes. Kein Linear Color Space Blending. Kein garnichts. Trivial wäre:
return {
l.m128i_u8[0] * r.m128i_u8[0] / 255,
l.m128i_u8[1] * r.m128i_u8[1] / 255,
l.m128i_u8[2] * r.m128i_u8[2] / 255,
l.m128i_u8[3] * r.m128i_u8[3] / 255
};
Im Internet findet man viele Leute, die nicht einmal
das schaffen. Die teilen durch 256. WTF?!
255 * 255 / 256 != 255. Eure Farben werden mit jedem Blending dunkler. Aber was weiß ich schon – ihr könnt eine Division durch Bit Shifting ersetzen. Ihr kennt euch mit Optimierung aus.
Die Befehle, die Visual C++ dafür ausspuckt, sind nicht der Rede wert: 37 Stück. Jeder
unsigned char wird extrahiert, multipliziert, dividiert (die Division wird zu Shifts/Addition/Subtraktion optimiert), dann wieder zurückgeschrieben. Einerseits enttäuschend, andererseits haben wir aber die oberen 12 Bytes nicht definiert und der Compiler denkt nun, dass wir da unbedingt Nullen drin haben wollen und damit ist das alles schlecht optimierbar.
Auf Clang/GCC kompiliert das nicht, da muss man deren Vektorsyntax verwenden. Erstmal GCC mit 4×
unsigned char:
using UINT1BX4 = unsigned char __attribute__((vector_size(4)));
UINT1BX4 mul_r8g8b8a8(UINT1BX4 l, UINT1BX4 r) {
return l * r / 255;
}
Etwa gleich viele Befehle wie Visual C++, nur dass die Vektorregister komplett übergangen werden. RICHTIG katastrophal auf Clang (mit richtigen skalaren Divisionen, man kann’s sich nicht ausdenken!). Erzwingen wir SSE durch 16 Vektorkomponenten:
using UINT1BX4 = unsigned char __attribute__((vector_size(16)));
UINT1BX4 mul_r8g8b8a8(UINT1BX4 const l, UINT1BX4 const r) {
return l * r / (UINT1BX4){ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 };
}
(ohne die lange Liste von 255ern frisst Clang es nicht)
29 Befehle in Clang; rund 125 in GCC. Beide fangen nun an, Vektorbefehle zu benutzen und z.B. vier Zahlen parallel zu multiplizieren. Nicht, dass es was helfen würde. Füttern wir nochmal genau die gleiche Beschreibung rein wie damals Visual C++, nur in Clang/GCC-Syntax:
using UINT1BX4 = unsigned char __attribute__((vector_size(16)));
UINT1BX4 mul_r8g8b8a8(UINT1BX4 const l, UINT1BX4 const r) {
return (UINT1BX4){
(unsigned char)(l[0] * r[0] / 255),
(unsigned char)(l[1] * r[1] / 255),
(unsigned char)(l[2] * r[2] / 255),
(unsigned char)(l[3] * r[3] / 255)
};
}
33 Befehle in Clang, 45 in GCC, und beide umgehen Vektorisierung fast komplett.
Erstmal ist sicher: Autovektorisierung, SIMD-Syntax usw. kann man in die Tonne kloppen. Will man ordentlichen SIMD-Code, muss man von Hand Intrinsics aufrufen. Dummerweise ist das nur in Visual C++ einfach; in GCC noch umständlich möglich; in Clang quasi unmöglich.
„Einfache“ Intrinsics à
„multiplizier den Vektor hier mit dem da“ gibt es nur, so lange man mit 4×float arbeitet. In alles andere muss man sich erstmal einarbeiten (es gibt um die 20 verschiedenen Intrinsics für Integermultiplikation, und fast keiner davon berechnet tatsächlich das Produkt).
Hier der beste skalare Compiler (Clang im letzten Versuch):
Code: Alles auswählen
movdqa xmmword ptr [rsp - 40], xmm0
movzx r8d, byte ptr [rsp - 37]
movzx r9d, byte ptr [rsp - 38]
movzx r10d, byte ptr [rsp - 39]
movzx edi, byte ptr [rsp - 40]
movaps xmmword ptr [rsp - 24], xmm1
movzx edx, byte ptr [rsp - 21]
movzx esi, byte ptr [rsp - 22]
movzx eax, byte ptr [rsp - 23]
movzx ecx, byte ptr [rsp - 24]
imul ecx, edi
mov edi, 2155905153
imul rcx, rdi
shr rcx, 39
imul eax, r10d
imul rax, rdi
shr rax, 39
imul esi, r9d
imul rsi, rdi
shr rsi, 39
imul edx, r8d
imul rdx, rdi
shr rdx, 39
shl edx, 8
movzx esi, sil
or esi, edx
shl eax, 8
movzx ecx, cl
or ecx, eax
pxor xmm0, xmm0
pinsrw xmm0, ecx, 0
pinsrw xmm0, esi, 1
ret
Hier von Hand aufgerufene Intrinsics in Visual C++:
Code: Alles auswählen
movaps xmm3, xmm0
xorps xmm2, xmm2
movaps xmm4, xmm1
punpcklbw xmm3, xmm2
punpcklbw xmm4, xmm2
pmullw xmm4, xmm3
pmulhuw xmm4, XMMWORD PTR __xmm@80818081808180818081808180818081
psrlw xmm4, 7
packuswb xmm4, xmm4
movaps xmm0, xmm4
ret 0
Die Division durch 255 muss man von Hand in Multiplikation + Shift umwandeln, weil das kein Compiler außer GCC mit Vektoren kann.