[C++] Mikrooptimierungs-Log
Forumsregeln
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
Möglichst sinnvolle Präfixe oder die Themensymbole nutzen.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Die Intel-Intrinsics sind bei GCC keine Intrinsics, sondern stecken in Headern – und fremde Header benutze ich nicht.
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: Jammer-Thread
Ich sehe nicht den großen Unterschied zwischen Builtin Headern und Builtin Funktionen, aber ok wie du meinst.
Nur vielleicht solltest du dich dann nicht beschweren, denn das ist schon ein sehr extremer und ungewöhnlicher Ansatz. ;)
Nur vielleicht solltest du dich dann nicht beschweren, denn das ist schon ein sehr extremer und ungewöhnlicher Ansatz. ;)
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Wie kriege ich eigentlich ein einzelnes float in einen SSE-Datentyp, ohne zusätzlichen Code zu erzeugen?
Alle Compiler (Clang, GCC, VCpp) haben floats sowieso in einem XMM-Register liegen. Intrinsics wie _mm_set_ss() sollten daher ein No-Op sein. Sind sie aber mit keinem einzigen Compiler!
Das liest sich wie eine Optimizer-Karikatur! Clang kriegt nicht einmal zwei genullte Register zu einem kombiniert (bei Visual C++ bin ich das längst gewöhnt). Man sieht, dass da NULL optimiert wurde (der Optimizer scheint das Nullen der drei oberen Lanes als sehr wichtig zu empfinden, obwohl sie hinterher alle wieder weggeschmissen werden).
Am nächsten dran bin ich via _mm_set_ps(x, x, x, x). Statt xorps + movss habe ich dann shufps (hier können die Compiler plötzlich DOCH Kopien wegoptimieren!). Warum kriegt man diesen überflüssigen Befehl nicht auch noch weg? Warum muss ich so viel Overhead hinscheißen, nur um MINPS benutzen zu dürfen? Warum machen die alle ihre Jobs nicht richtig?
Alle Compiler (Clang, GCC, VCpp) haben floats sowieso in einem XMM-Register liegen. Intrinsics wie _mm_set_ss() sollten daher ein No-Op sein. Sind sie aber mit keinem einzigen Compiler!
Code: Alles auswählen
float minimumOf(float a, float b) {
return _mm_cvtss_f32(_mm_min_ps(
_mm_set_ss(a),
_mm_set_ss(b)
));
}
Code: Alles auswählen
; Clang
xorps xmm3, xmm3
xorps xmm2, xmm2
movss xmm2, xmm0 # xmm2 = xmm0[0],xmm2[1,2,3]
movss xmm3, xmm1 # xmm3 = xmm1[0],xmm3[1,2,3]
minps xmm2, xmm3
movaps xmm0, xmm2
ret
; GCC
pxor xmm2, xmm2
movss DWORD PTR [rsp-12], xmm0
movss xmm0, DWORD PTR [rsp-12]
movss xmm2, xmm1
minps xmm0, xmm2
ret
; VCpp
movaps xmm2, xmm0
xorps xmm3, xmm3
movss xmm3, xmm2
xorps xmm2, xmm2
movss xmm2, xmm1
minps xmm3, xmm2
movaps xmm0, xmm3
ret 0
Am nächsten dran bin ich via _mm_set_ps(x, x, x, x). Statt xorps + movss habe ich dann shufps (hier können die Compiler plötzlich DOCH Kopien wegoptimieren!). Warum kriegt man diesen überflüssigen Befehl nicht auch noch weg? Warum muss ich so viel Overhead hinscheißen, nur um MINPS benutzen zu dürfen? Warum machen die alle ihre Jobs nicht richtig?
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: Jammer-Thread
Ja, das ist tatsächlich ziemlich albern.
Interessant ist das Clang den idealen Code erzeugt, sobald man _mm_min_ss verwendet (d.h. nur eine einzige Instruction).
Es ist nicht immer möglich, aber so eine Funktion setzt man am Besten einfach in standard C++ um.
Generiert auf allen 3 Compilern den optimalen Code.
Interessant ist das Clang den idealen Code erzeugt, sobald man _mm_min_ss verwendet (d.h. nur eine einzige Instruction).
Es ist nicht immer möglich, aber so eine Funktion setzt man am Besten einfach in standard C++ um.
Code: Alles auswählen
float minimumOf(float a, float b) {
return a < b ? a : b;
}
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Nein; der optimale Code wäre MINPS, aber alle drei erzeugen MINSS.
Ich verwende das auch, soweit möglich (und das ist eher zum Anti-Jammern, denn vor drei, vier Jahren bekam man da grundsätzlich nur via Assembly gutes Kompilat). Für das hier erzeugt auch jeder Compiler CVTTSS2SI (ich kann also meine Intrinsics-Version von vor drei Jahren löschen):
int truncated(float x) { return int(f); }
Hier erzeugen alle drei ROR:
unsigned int rotated_right(unsigned int x, int bits) {
return (x >> bits) | (x << (32 - bits));
}
Beachtet, dass der Code Undefined Behavior enthält (bei bits == 0 wird um 32 Bits nach links geschoben -> UB). Umso erstaunlicher, dass alle Compiler die richtige Interpretation hard-coded haben, obwohl Clang und GCC warnen. Pappt man ein Modulo 32 (oder & 31 oder wasauchimmer) an den Shift, versteht Visual C++ es überhaupt nicht mehr, aber man ist standardkonform und Clang/GCC erzeugen gutes Kompilat. Hier also unbedingt separaten VC-Pfad mit _rotr()-Intrinsic.
Hier erzeugt Visual C++ ANDPS mit Maske:
float absoluteOf(float x) { return x > 0 ? x : -x; }
aber weder Clang noch GCC erkennen es und man muss an der Stelle __builtin_fabs() einsetzen, damit nicht gebrancht wird.
Bei allem anderen bin ich total gefickt: Ich kriege immer mindestens ein überflüssiges SHUFPS beim Cast von float zu __m128. Oder ein überflüssiges movd beim Cast von double zu __m128d. Und entsprechenden Registerdruck weil jeder Parameter doppelt vorliegt. Totaler Schwachsinn.
Ich verwende das auch, soweit möglich (und das ist eher zum Anti-Jammern, denn vor drei, vier Jahren bekam man da grundsätzlich nur via Assembly gutes Kompilat). Für das hier erzeugt auch jeder Compiler CVTTSS2SI (ich kann also meine Intrinsics-Version von vor drei Jahren löschen):
int truncated(float x) { return int(f); }
Hier erzeugen alle drei ROR:
unsigned int rotated_right(unsigned int x, int bits) {
return (x >> bits) | (x << (32 - bits));
}
Beachtet, dass der Code Undefined Behavior enthält (bei bits == 0 wird um 32 Bits nach links geschoben -> UB). Umso erstaunlicher, dass alle Compiler die richtige Interpretation hard-coded haben, obwohl Clang und GCC warnen. Pappt man ein Modulo 32 (oder & 31 oder wasauchimmer) an den Shift, versteht Visual C++ es überhaupt nicht mehr, aber man ist standardkonform und Clang/GCC erzeugen gutes Kompilat. Hier also unbedingt separaten VC-Pfad mit _rotr()-Intrinsic.
Hier erzeugt Visual C++ ANDPS mit Maske:
float absoluteOf(float x) { return x > 0 ? x : -x; }
aber weder Clang noch GCC erkennen es und man muss an der Stelle __builtin_fabs() einsetzen, damit nicht gebrancht wird.
Bei allem anderen bin ich total gefickt: Ich kriege immer mindestens ein überflüssiges SHUFPS beim Cast von float zu __m128. Oder ein überflüssiges movd beim Cast von double zu __m128d. Und entsprechenden Registerdruck weil jeder Parameter doppelt vorliegt. Totaler Schwachsinn.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Ach, noch einer, mit dem man wirklich tief in die Innereien der Compilers gucken kann: Bit Scan. Man sucht den Index des höchsten Bits, das gesetzt ist. Dann weiß man, wie viele Bits die Zahl hat. Bei 13 (binär 1101) wäre das also 3, weil das 4. Bit von hinten das höchstwertige ist, das gesetzt ist.
Hier gibt es auch wieder einen Hardware-Befehl: BSR (bit scan reverse).
Visual C++ hat das _BitScanReverse64()-Intrinsic. Alles gut.
Clang/GCC haben dafür kein Intrinsic. Die haben nur __builtin_clzll(), und das zählt die Anzahl der Nullen von vorn nach hinten. Also das Gegenteil.
Im Internet bekommt man gern den Hinweis, dass 63 - __builtin_clzll() das gleiche ergibt wie _BitScanReverse64(). Stimmt auch.
Clang erzeugt auch optimal Code: einen einzigen BSR-Befehl.
GCC nicht:
bsr rdi, rdi
mov eax, 63
xor rdi, 63
sub eax, edi
XOR? SUB? WTF?
Irgendein Genie bei GCC muss sich mal gedacht haben:
Die Lösung ist also:
return 63 ^ __builtin_clzll(x);
denn das erzeugt 63 ^ 63 ^ BSR(x) und kann auch von GCC zu BSR(x) optimiert werden. Dann kriegt man unter Clang und GCC optimales Kompilat.
Kurzer Reality Check, wofür man diese Zählanweisungen nutzen kann (außer für Voxel Engines): SIMD-Parsing von Textdateien. Will ich Whitespace überspringen, lade ich 16 Buchstaben in ein SSE-Register, und prüfe die 16 ASCII-Werte parallel auf Leerzeichen/Tab/etc. Dabei wird alles auf 0 gesetzt, was Whitespace ist, und alles andere auf 1. Die höchstwertigen Bits jeder Spur extrahiere ich (dafür gibt’s einen Hardware-Befehl), und kann via BSF (Nullen am Ende der Zahl) zählen, wie weit es bis zum nächsten Bezeichner ist (mit phänomenalem Nutzen-pro-Takt-Verhältnis).
Hier gibt es auch wieder einen Hardware-Befehl: BSR (bit scan reverse).
Visual C++ hat das _BitScanReverse64()-Intrinsic. Alles gut.
Clang/GCC haben dafür kein Intrinsic. Die haben nur __builtin_clzll(), und das zählt die Anzahl der Nullen von vorn nach hinten. Also das Gegenteil.
Im Internet bekommt man gern den Hinweis, dass 63 - __builtin_clzll() das gleiche ergibt wie _BitScanReverse64(). Stimmt auch.
Clang erzeugt auch optimal Code: einen einzigen BSR-Befehl.
GCC nicht:
bsr rdi, rdi
mov eax, 63
xor rdi, 63
sub eax, edi
XOR? SUB? WTF?
Irgendein Genie bei GCC muss sich mal gedacht haben:
- __builtin_clzll(x) wird emuliert durch 63 - BSR(x)
- die Subtraktion ist zu langsam/zu lang/blockiert den falschen Port
- also nehmen wir 63 ^ BSR(x), denn für Werte im Bereich [0, 63] mit Zweierkomplement und blabla ist XOR das gleiche wie Subtraktion und der Befehl ist schneller/kürzer/nutzt ’nen freien Port
Die Lösung ist also:
return 63 ^ __builtin_clzll(x);
denn das erzeugt 63 ^ 63 ^ BSR(x) und kann auch von GCC zu BSR(x) optimiert werden. Dann kriegt man unter Clang und GCC optimales Kompilat.
Kurzer Reality Check, wofür man diese Zählanweisungen nutzen kann (außer für Voxel Engines): SIMD-Parsing von Textdateien. Will ich Whitespace überspringen, lade ich 16 Buchstaben in ein SSE-Register, und prüfe die 16 ASCII-Werte parallel auf Leerzeichen/Tab/etc. Dabei wird alles auf 0 gesetzt, was Whitespace ist, und alles andere auf 1. Die höchstwertigen Bits jeder Spur extrahiere ich (dafür gibt’s einen Hardware-Befehl), und kann via BSF (Nullen am Ende der Zahl) zählen, wie weit es bis zum nächsten Bezeichner ist (mit phänomenalem Nutzen-pro-Takt-Verhältnis).
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: Jammer-Thread
Hm, habe ich was verpasst?
Warum ist minps besser als minss?
Bei float absoluteOf(float x) { return x > 0 ? x : -x; } muss ich leider GCC/Clang rechtgeben, denn die elegante Version mit and verändert das Vorzeichen wenn x = NaN ist. Clang optimiert den Code erfolgreich mit -ffinite-math-only und GCC zumindest mit -ffast-math.
Godbolt
EDIT:
Eine standardsicher Version ohne builtin:
Allerdings ein externer Header und Microsoft ist hier auch völlig überfordert und generiert sogar einen Funktionsaufruf. :roll:
Warum ist minps besser als minss?
Bei float absoluteOf(float x) { return x > 0 ? x : -x; } muss ich leider GCC/Clang rechtgeben, denn die elegante Version mit and verändert das Vorzeichen wenn x = NaN ist. Clang optimiert den Code erfolgreich mit -ffinite-math-only und GCC zumindest mit -ffast-math.
Godbolt
EDIT:
Eine standardsicher Version ohne builtin:
Code: Alles auswählen
#include <math.h>
float absoluteOf(float x)
{
return copysignf(x, 1.0f);
}
Zuletzt geändert von Spiele Programmierer am 07.08.2017, 23:16, insgesamt 2-mal geändert.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Die Latenz ist gleich, aber es ist ein Byte kürzer.Spiele Programmierer hat geschrieben:Hm, habe ich was verpasst?
Warum ist minps besser als minss?
Verdammt – ich muss hier noch /fp:fast an gehabt haben; da hast du völlig recht!Spiele Programmierer hat geschrieben:Bei float absoluteOf(float x) { return x > 0 ? x : -x; } muss ich leider GCC/Clang rechtgeben, denn die elegante Version mit and verändert das Vorzeichen wenn x = NaN ist. Clang optimiert den Code erfolgreich mit -ffinite-math-only und GCC zumindest mit -ffast-math.
Godbolt
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: Jammer-Thread
Also auf Godbolt kompilert Visual C++ das tatsächlich zu andps ohne weitere Flags... :roll:Verdammt – ich muss hier noch /fp:fast an gehabt haben; da hast du völlig recht!
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Anmerkung – für NaN selber hat das Vorzeichen keinen Einfluss. Mantisse und Vorzeichen stehen der Anwendung zur Verfügung, um darin Informationen zu speichern. Wikipedia sagt nun:Spiele Programmierer hat geschrieben:Bei float absoluteOf(float x) { return x > 0 ? x : -x; } muss ich leider GCC/Clang rechtgeben, denn die elegante Version mit and verändert das Vorzeichen wenn x = NaN ist.
Ich habe keinen Bock, mich jetzt durch den Standard zu wühlen, aber … wenn der Standard beim Wegschmeißen des Sign Bits bei abs keine Exception vorschreibt, würde ich zumindest die Möglichkeit einräumen, dass die Optimierung legal ist. So lange wir keine Paragraphen dazu haben, gebe ich aber weiter GCC/Clang recht.https://en.wikipedia.org/wiki/NaN hat geschrieben:The treatment of the sign bit of NaNs for some simple operations (such as absolute value) is different from that for arithmetic operations. Traps are not required by the standard.
- xq
- Establishment
- Beiträge: 1581
- Registriert: 07.10.2012, 14:56
- Alter Benutzername: MasterQ32
- Echter Name: Felix Queißner
- Wohnort: Stuttgart & Region
- Kontaktdaten:
Re: Jammer-Thread
Junge Junge Junge, ihr diskutiert hier auf nem ganz schön hohen Niveau! Mal wieder alles sehr lehrreich!
War mal MasterQ32, findet den Namen aber mittlerweile ziemlich albern…
Programmiert viel in Zig und nervt Leute damit.
Programmiert viel in Zig und nervt Leute damit.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Ich freue mich auch, dass Spiele Programmierer mit mir spricht; endlich mal Bidirektion im Jammer-Thread :P
- Schrompf
- Moderator
- Beiträge: 4855
- Registriert: 25.02.2009, 23:44
- Benutzertext: Lernt nur selten dazu
- Echter Name: Thomas Ziegenhagen
- Wohnort: Dresden
- Kontaktdaten:
Re: Jammer-Thread
Und würde vielleicht in den Mikrooptimierungs-Thread gehören. Ich finde es aber auch gerade sehr spannend, was ihr hier so ausbreitet. Gerade weil ich die ganze Zeit meine Voxel-Engine im Hinterkopf habe, der ich irgendwann mit Hardcore-SS2 Beine machen muss. Oder war's SSE4...? Irgendeins der Bitscan-Befehle war erst übelst spät eingeführt worden, wie ich anhand von Splatter-ILLEGAL INSTRUCTION-Testberichten feststellen durfte, obwohl die Kumpels, die für mich gefühlt zur selben Gattung gehören, schon seit SSE1 verfügbar sind.
Früher mal Dreamworlds. Früher mal Open Asset Import Library. Heutzutage nur noch so rumwursteln.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Es war SSE4.1, Population Count, und bei mir hattest du auch mal was AVX-Kompiliertes gegeben ;)
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Hat schonmal jemand bei Clang/GCC __attribute((vector_size())) mit was anderem als 16 benutzt?
Ich möchte gern vier unsigned chars parallel verarbeiten. Ideal dafür wären SSE-Register (trotz 75 % Platzverschwendung – volles SIMD und sogar Saturation Arithmetic, fuck yea!)
Ich habe ganz ganz schnell mit Clang & GCC getestet, wie sie unsigned char __attribute((vector_size(4))) behandeln, und …
GCC bekommt einen Trostpreis für seinen Fleiß, alle vier Werte parallel in einem General Purpose Register zu addieren. Das ist sehr clever.
Clang schmeißt GPR und SSE auf schlimmstmögliche Art durcheinander und kriegt garnichts.
Vielleicht ist das Versagen dem Umstand geschuldet, dass die Compiler gezwungen sind, sizeof(UINT1BX4) == 4 zu garantieren, und die Werte deshalb nicht in den SSE-Registern parken dürfen?
Soll ich einfach 16er-Vektoren nutzen und die oberen zwölf Elemente ignorieren? Dann landen die Werte garantiert in SSE-Registern und beide Compiler erzeugen in einfachen Fällen hervorragenden Code; ich weiß nur nicht, wie die komplizierten Fälle aussehen werden …
Ich möchte gern vier unsigned chars parallel verarbeiten. Ideal dafür wären SSE-Register (trotz 75 % Platzverschwendung – volles SIMD und sogar Saturation Arithmetic, fuck yea!)
Ich habe ganz ganz schnell mit Clang & GCC getestet, wie sie unsigned char __attribute((vector_size(4))) behandeln, und …
GCC bekommt einen Trostpreis für seinen Fleiß, alle vier Werte parallel in einem General Purpose Register zu addieren. Das ist sehr clever.
Clang schmeißt GPR und SSE auf schlimmstmögliche Art durcheinander und kriegt garnichts.
Vielleicht ist das Versagen dem Umstand geschuldet, dass die Compiler gezwungen sind, sizeof(UINT1BX4) == 4 zu garantieren, und die Werte deshalb nicht in den SSE-Registern parken dürfen?
Soll ich einfach 16er-Vektoren nutzen und die oberen zwölf Elemente ignorieren? Dann landen die Werte garantiert in SSE-Registern und beide Compiler erzeugen in einfachen Fällen hervorragenden Code; ich weiß nur nicht, wie die komplizierten Fälle aussehen werden …
Re: Jammer-Thread
Dem kann ich mich nur anschliessen.MasterQ32 hat geschrieben:Junge Junge Junge, ihr diskutiert hier auf nem ganz schön hohen Niveau! Mal wieder alles sehr lehrreich!
Und wünsche mir dass ihr resp Krishty seine Entdeckungen auch jeweils den Jungs von Clang/LLVM
meldet damit ich irgendwann auch indirekt davon profitiere.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
Ganz ehrlich … die Zeit habe ich nicht. Wenn ich ein Patreon aufmache, und ihr mich dafür durchfüttert, dann vielleicht. Aber jetzt nicht auch noch Bug Tracker füttern, bitte nicht. Das kann jemand machen, der hier mitliest.
Ich habe eben fast eine Stunde(!) an der Konvertierung von unsigned char zu unsigned int gesessen. Dass GCC zwei Register nullt statt einem, das muss jemand anders melden. Ich krieg’s nicht gelöst und mein Hirn ist erstmal Matsche.
GCC erzeugt
pxor xmm1, xmm1
punpcklbw xmm0, xmm1
pxor xmm1, xmm1
punpcklwd xmm0, xmm1
ret
und ich denke nicht, dass das doppelte pxor xmm1, xmm1 (Register auf Null setzen) eine blöde verpasste Optimierung ist, sondern dass im Gegenteil GCC zu stark zu optimieren versucht – wahrscheinlich etwas wie „das zweite Nullen kann ich die CPU unabhängig vom ersten ausführen lassen, damit es schneller fertig wird“ ohne zu begreifen, dass die Befehle sowieso aufeinander aufbauen und man deshalb nichts nebenläufig schedulen kann.
1.800 Zeichen, um drei Assembler-Befehle zu erzeugen. Mit direkter Nutzung der Intrinsic-Header (was jeder tun würde, der nicht bekloppt wie ich ist) wären es immernoch 665. Den Assembler-Code direkt hinschreiben wären … weniger als 100. Aber das darf ich ja nicht, denn Inline Assembly verwirrt die Compiler! Zum Kotzen. Einfach nur zum Kotzen. Seht, wie viel Zeit sie uns gespart haben mit ihrer portablen, leicht verständlichen, allmächtigen, Vektorsyntax! (CAST)__builtin_shufflevector((CAST)am, (CAST)arsch, 0, 13, 4, 9, 42, 36, Superzahl 666, achlecktmichdoch
Ich habe eben fast eine Stunde(!) an der Konvertierung von unsigned char zu unsigned int gesessen. Dass GCC zwei Register nullt statt einem, das muss jemand anders melden. Ich krieg’s nicht gelöst und mein Hirn ist erstmal Matsche.
Code: Alles auswählen
// Expands all elements to 4-B signed integers.
SInt4Bx4 SIMD_CALL asSInt4B(UInt1Bx4 const v) {
// • interleave each 1-B lane with three zero bytes
// – first interleave each 1-B lane with 1 B of zeroes via SSE2’s PUNPCKLBW
// – then interleave each 2-B lane with 2 B of zeroes via SSE2’s PUNPCKLWD
# if COMPILED_BY_VISUAL_CPP
// • PUNPCKLBW is available via “_mm_unpacklo_epi8()” intrinsic
// • PUNPCKLWD is available via “_mm_unpacklo_epi16()” intrinsic
auto const v0v0v0v0 = _mm_unpacklo_epi8(v.uints, _mm_setzero_si128());
auto const v000v000v000v000 = _mm_unpacklo_epi16(v0v0v0v0, _mm_setzero_si128());
return { v000v000v000v000 };
# elif COMPILED_BY_CLANG
// • Clang’s code generation miserably fails vector initializer lists and vector casting
// • no intrinsics available; need to use “__builtin_shufflevector()” instead
auto const v0v0v0v0 = (IMPL_UINT2BX8)__builtin_shufflevector(v.uints, IMPL_UINT1BX16(), 0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23);
auto const v000v000v000v000 = (IMPL_SINT4BX4)__builtin_shufflevector(v0v0v0v0, IMPL_UINT2BX8(), 0, 8, 1, 9, 2, 10, 3, 11);
return { v000v000v000v000 };
# elif COMPILED_BY_GCC
// • PUNPCKLBW is available via “__builtin_ia32_punpcklbw128()” intrinsic
// • PUNPCLWD is available via “__builtin_ia32_punpcklwd128()” intrinsic
// • code generation fails miserably with anything else
// • TODO: for some reason, GCC zeroes two registers instead of one (not even Visual C++ fails here)
auto const v0v0v0v0 = (IMPL_SINT2BX8)__builtin_ia32_punpcklbw128((IMPL_CHARX16)v.uints, IMPL_CHARX16());
auto const v000v000v000v000 = (IMPL_SINT4BX4)__builtin_ia32_punpcklwd128(v0v0v0v0, IMPL_SINT2BX8());
return { v000v000v000v000 };
# endif
}
pxor xmm1, xmm1
punpcklbw xmm0, xmm1
pxor xmm1, xmm1
punpcklwd xmm0, xmm1
ret
und ich denke nicht, dass das doppelte pxor xmm1, xmm1 (Register auf Null setzen) eine blöde verpasste Optimierung ist, sondern dass im Gegenteil GCC zu stark zu optimieren versucht – wahrscheinlich etwas wie „das zweite Nullen kann ich die CPU unabhängig vom ersten ausführen lassen, damit es schneller fertig wird“ ohne zu begreifen, dass die Befehle sowieso aufeinander aufbauen und man deshalb nichts nebenläufig schedulen kann.
1.800 Zeichen, um drei Assembler-Befehle zu erzeugen. Mit direkter Nutzung der Intrinsic-Header (was jeder tun würde, der nicht bekloppt wie ich ist) wären es immernoch 665. Den Assembler-Code direkt hinschreiben wären … weniger als 100. Aber das darf ich ja nicht, denn Inline Assembly verwirrt die Compiler! Zum Kotzen. Einfach nur zum Kotzen. Seht, wie viel Zeit sie uns gespart haben mit ihrer portablen, leicht verständlichen, allmächtigen, Vektorsyntax! (CAST)__builtin_shufflevector((CAST)am, (CAST)arsch, 0, 13, 4, 9, 42, 36, Superzahl 666, achlecktmichdoch
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: Jammer-Thread
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):Hier von Hand aufgerufene Intrinsics in Visual C++:Die Division durch 255 muss man von Hand in Multiplikation + Shift umwandeln, weil das kein Compiler außer GCC mit Vektoren kann.
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
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
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Ich habe endlich einen großen Vorteil von Clangs Syntax gefunden: Shuffling mit undefinierten Elementen.
Liegt meine RGBA-Farbe in einem 16-Byte-SSE-Register, sind die zwölf oberen Spuren unbenutzt. Sagen wir, ich möchte die Alpha-Komponente in den unteren vier Spuren haben, und die oberen zwölf interessieren mich nicht.
Schreibe ich return { rgba[3], rgba[3], rgba[3], rgba[3] }, muss der Compiler garantieren, dass die oberen zwölf Spuren genullt sind. So fordert das C++. Dem Compiler ist nicht vermittelbar, dass sie unbenutzt sind. Das Nullen bedeutet zusätzlichen Aufwand.
Schreibe ich return { rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3] }, entfällt das Nullen. Der Compiler verbaut aber immernoch zu viele Befehle, um die dritte Spur nach ganz oben zu schieben.
Clang glänzt mit return { __builtin_shufflevector(rgba, rgba, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }. Das -1 bedeutet, dass die entsprechende Spur undefiniert ist und der Compiler frei Befehle auswählen kann, so lange die unteren vier Spuren korrekt befüllt werden.
Das ist die erste Situation, in der ein Compiler meine selbstgeschriebene Intrinsic-Version deutlich abhängt. Was Clang hier teilweise über recht abwegige Befehle rausholen kann, ist beachtlich.
__builtin_shufflevector() ist auf GCC nicht verfügbar und AFAIK gibt es auch kein entsprechendes Pendant. Visual C++ kann sowieso nichts anderes als Intrinsics. Ich empfehle also, bei sowas immer erst eine Clang-Version mit __builtin_shufflevector() und undefinierten Elementen zu machen, und dann das resultierende Disassembly auf Intrinsics in GCC und Visual C++ zu übertragen.
Liegt meine RGBA-Farbe in einem 16-Byte-SSE-Register, sind die zwölf oberen Spuren unbenutzt. Sagen wir, ich möchte die Alpha-Komponente in den unteren vier Spuren haben, und die oberen zwölf interessieren mich nicht.
Schreibe ich return { rgba[3], rgba[3], rgba[3], rgba[3] }, muss der Compiler garantieren, dass die oberen zwölf Spuren genullt sind. So fordert das C++. Dem Compiler ist nicht vermittelbar, dass sie unbenutzt sind. Das Nullen bedeutet zusätzlichen Aufwand.
Schreibe ich return { rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3], rgba[3] }, entfällt das Nullen. Der Compiler verbaut aber immernoch zu viele Befehle, um die dritte Spur nach ganz oben zu schieben.
Clang glänzt mit return { __builtin_shufflevector(rgba, rgba, 3, 3, 3, 3, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 }. Das -1 bedeutet, dass die entsprechende Spur undefiniert ist und der Compiler frei Befehle auswählen kann, so lange die unteren vier Spuren korrekt befüllt werden.
Das ist die erste Situation, in der ein Compiler meine selbstgeschriebene Intrinsic-Version deutlich abhängt. Was Clang hier teilweise über recht abwegige Befehle rausholen kann, ist beachtlich.
__builtin_shufflevector() ist auf GCC nicht verfügbar und AFAIK gibt es auch kein entsprechendes Pendant. Visual C++ kann sowieso nichts anderes als Intrinsics. Ich empfehle also, bei sowas immer erst eine Clang-Version mit __builtin_shufflevector() und undefinierten Elementen zu machen, und dann das resultierende Disassembly auf Intrinsics in GCC und Visual C++ zu übertragen.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Ich weiß nicht, was in den Compilern vorgeht. Zur Hölle, ich verstehe nicht, wie sowas passieren kann.
void store(UInt1B memory[4], UINT1BX4 vector) {
memory[0] = vector[0];
memory[1] = vector[1];
memory[2] = vector[2];
memory[3] = vector[3];
}
Das sollte ein einziges MOVD oder MOVSS sein – die unteren 4 B in einem Rutsch in den Speicher schreiben. Stattdessen Clang:
Visual C++:
GCC (WTF):
… nun muss ich wieder einen zehn-Zeilen-Aufsatz schreiben:
void store(UInt1B memory[4], UInt1Bx4 const v) {
// • try to store four lanes at once via SSE2’s MOVD
// • this works only when treating the four lowest lanes as a single 4-B integer
# if COMPILED_BY_VISUAL_CPP
// • the “_mm_cvtsi128_si32()” intrinsic casts the four lowest lanes to a 4-B integer
*reinterpret_cast<int *>(memory) = _mm_cvtsi128_si32(v);
# elif COMPILED_BY_CLANG || COMPILED_BY_GCC
// • “reinterpret_cast” does not violate strict aliasing here because 1-B types may alias anything
// • GCC’s/Clang’s code generation miserably fails anything else
*reinterpret_cast<UInt4B *>(memory) = ((UINT4BX4)v)[0];
# endif
}
… und was mache ich mit Strict Aliasing und 16-Bit-Zahlen? Zu Bjarne Stroustrup beten und ihm zum Opfer meinen Computer verbrennen?
void store(UInt1B memory[4], UINT1BX4 vector) {
memory[0] = vector[0];
memory[1] = vector[1];
memory[2] = vector[2];
memory[3] = vector[3];
}
Das sollte ein einziges MOVD oder MOVSS sein – die unteren 4 B in einem Rutsch in den Speicher schreiben. Stattdessen Clang:
Code: Alles auswählen
movaps xmmword ptr [rsp - 24], xmm0
mov sil, byte ptr [rsp - 21]
mov cl, byte ptr [rsp - 22]
mov dl, byte ptr [rsp - 23]
mov al, byte ptr [rsp - 24]
mov byte ptr [rdi], al
mov byte ptr [rdi + 1], dl
mov byte ptr [rdi + 2], cl
mov byte ptr [rdi + 3], sil
ret
Code: Alles auswählen
movd eax, xmm1
movdqa xmm0, xmm1
psrldq xmm0, 1
mov BYTE PTR [rcx], al
movd eax, xmm0
movdqa xmm0, xmm1
psrldq xmm0, 2
psrldq xmm1, 3
mov BYTE PTR [rcx+1], al
movd eax, xmm0
mov BYTE PTR [rcx+2], al
movd eax, xmm1
mov BYTE PTR [rcx+3], al
ret 0
Code: Alles auswählen
movaps XMMWORD PTR [rsp-72], xmm0
movzx eax, BYTE PTR [rsp-72]
movaps XMMWORD PTR [rsp-40], xmm0
movaps XMMWORD PTR [rsp-56], xmm0
mov BYTE PTR [rdi], al
movzx eax, BYTE PTR [rsp-39]
mov BYTE PTR [rdi+1], al
movzx eax, BYTE PTR [rsp-54]
mov BYTE PTR [rdi+2], al
movzx eax, BYTE PTR [rsp-69]
mov BYTE PTR [rdi+3], al
ret
void store(UInt1B memory[4], UInt1Bx4 const v) {
// • try to store four lanes at once via SSE2’s MOVD
// • this works only when treating the four lowest lanes as a single 4-B integer
# if COMPILED_BY_VISUAL_CPP
// • the “_mm_cvtsi128_si32()” intrinsic casts the four lowest lanes to a 4-B integer
*reinterpret_cast<int *>(memory) = _mm_cvtsi128_si32(v);
# elif COMPILED_BY_CLANG || COMPILED_BY_GCC
// • “reinterpret_cast” does not violate strict aliasing here because 1-B types may alias anything
// • GCC’s/Clang’s code generation miserably fails anything else
*reinterpret_cast<UInt4B *>(memory) = ((UINT4BX4)v)[0];
# endif
}
… und was mache ich mit Strict Aliasing und 16-Bit-Zahlen? Zu Bjarne Stroustrup beten und ihm zum Opfer meinen Computer verbrennen?
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: [C++] Mikrooptimierungs-Log
Das ist wirklich schrecklich.
Aber ich bin mir ziemlich sicher, dass dieser Code auch jetzt Strict Aliasing verletzt. Man darf zwar jeden Pointer in unsigned char* casten und damit zugreifen, aber man darf nicht in die andere Richtung jeden unsigned char* in einen beliebigen Typ casten und zugreifen. Der Pointer muss ursprünglich mit diesem Typ konstruiert/zugewiesen worden sein. Aus praktischer Sicht kann der Compiler nicht garantieren das der ursprüngliche char-Pointer das notwendige Alignment für einen anderen Typen hat. Außerdem könnte man sonst Strict Aliasing immer umgehen in dem man einfach einen Umweg über unsigned char* macht und die ganze Regel wäre zwecklos: reinterpret<T*>(reinterpret<unsigned char*>(...))
Die jeweiligen Standards sind da wohl nicht unbedingt ganz eindeutig zu interpretieren, aber der Konsens ist wohl, dass memcpy solche Probleme vermeidet: https://stackoverflow.com/questions/327 ... and-memcpy
Also das sollte gehen:
GCC und Clang erzeugen da perfekten Code.
Strict Aliasing ist immer eine sehr komplizierte Angelegenheit.und was mache ich mit Strict Aliasing und 16-Bit-Zahlen? Zu Bjarne Stroustrup beten und ihm zum Opfer meinen Computer verbrennen?
Aber ich bin mir ziemlich sicher, dass dieser Code auch jetzt Strict Aliasing verletzt. Man darf zwar jeden Pointer in unsigned char* casten und damit zugreifen, aber man darf nicht in die andere Richtung jeden unsigned char* in einen beliebigen Typ casten und zugreifen. Der Pointer muss ursprünglich mit diesem Typ konstruiert/zugewiesen worden sein. Aus praktischer Sicht kann der Compiler nicht garantieren das der ursprüngliche char-Pointer das notwendige Alignment für einen anderen Typen hat. Außerdem könnte man sonst Strict Aliasing immer umgehen in dem man einfach einen Umweg über unsigned char* macht und die ganze Regel wäre zwecklos: reinterpret<T*>(reinterpret<unsigned char*>(...))
Die jeweiligen Standards sind da wohl nicht unbedingt ganz eindeutig zu interpretieren, aber der Konsens ist wohl, dass memcpy solche Probleme vermeidet: https://stackoverflow.com/questions/327 ... and-memcpy
Also das sollte gehen:
Code: Alles auswählen
void store2(UInt1B memory[4], UINT1BX4 vector)
{
#if COMPILED_BY_VISUAL_CPP
*reinterpret_cast<int *>(memory) = _mm_cvtsi128_si32(v);
#else
memcpy(memory, &vector, 4); // Catastrophic code generation with Microsoft
#endif
}
Zuletzt geändert von Spiele Programmierer am 11.08.2017, 00:17, insgesamt 1-mal geändert.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Ich vergaß zu schreiben: Was Strict Aliasing angeht, machen es die Intrinsic-Implementierungen in Clang & GCC intern beim Laden so:
struct MOVD {
int i32;
} __attribute__((__packed__, __may_alias__));
return (UINT1BX4)(intx4){ ((MOVD const *)memory)->i32, 0, 0, 0 };
… und beim Speichern umgekehrt.
struct MOVD {
int i32;
} __attribute__((__packed__, __may_alias__));
return (UINT1BX4)(intx4){ ((MOVD const *)memory)->i32, 0, 0, 0 };
… und beim Speichern umgekehrt.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Das ist schon klar; in diesem Fall lade ich aber tatsächlich vier unsigned chars in einen Vektor von 16×unsigned chars, indem ich ihn kurz int uminterpretiere. Ich hätte gedacht, dass das legal ist; und schnellem Googeln nach ist es wirklich kompliziert – wahrscheinlich noch mehr, als den Compiler dazu zu bekommen, ordentlichen Code zu generieren :DSpiele Programmierer hat geschrieben:Das ist wirklich schrecklich.
Aber ich bin mir ziemlich sicher, dass dieser Code auch jetzt Strict Aliasing verletzt. Man darf zwar jeden Pointer in unsigned char* casten und damit zugreifen, aber man darf nicht in die andere Richtung jeden unsigned char* in einen beliebigen Typ casten und zugreifen. Der Pointer muss ursprünglich mit diesem Typ konstruiert/zugewiesen worden sein. Aus praktischer Sicht kann der Compiler nicht garantieren das der ursprüngliche char-Pointer das notwendige Alignment für einen anderen Typen hat. Außerdem könnte man sonst Strict Aliasing immer umgehen in dem man einfach einen Umweg über unsigned char* macht und die ganze Regel wäre zwecklos: reinterpret<T*>(reinterpret<unsigned char*>(...))
Danke für den memcpy()-Tipp; das probiere ich tatsächlich mal umzusetzen. Ich bin gespannt, wie gut/schlecht es z.B. mit oberen Hälften von Registern funktionieren wird.
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Ach, das wäre auch zu schön gewesen. Perfekter Code mit GCC, aber Clang sagt error: address of vector element requested. Für komplette Vektoren ist das Jackpot, aber für z.B. die obere Hälfte der Elemente muss ich weiter über struct __attribluärgh gehen.Krishty hat geschrieben:Danke für den memcpy()-Tipp; das probiere ich tatsächlich mal umzusetzen. Ich bin gespannt, wie gut/schlecht es z.B. mit oberen Hälften von Registern funktionieren wird.
-
- Establishment
- Beiträge: 426
- Registriert: 23.01.2013, 15:55
Re: [C++] Mikrooptimierungs-Log
Hm, also vielleicht verstehe ich dich falsch, aber auf Godbolt scheint es zu gehen:
Click
Click
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Argh, ich hatte &vector[12] ausprobiert! Danke :)
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Spiele,
schau dir bitte mal das hier an:
https://godbolt.org/g/azjxkX
Änder das #if 1 zu #if 0 und prüf, was rauskommt. Vielleicht ist es aber auch bloß spät und ich sehe den Wald vor lauter Bäumen nicht.
schau dir bitte mal das hier an:
https://godbolt.org/g/azjxkX
Änder das #if 1 zu #if 0 und prüf, was rauskommt. Vielleicht ist es aber auch bloß spät und ich sehe den Wald vor lauter Bäumen nicht.
- dot
- Establishment
- Beiträge: 1734
- Registriert: 06.03.2004, 18:10
- Echter Name: Michael Kenzel
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
Das ist wohl einfach Undefined Behavior; ich nehme an du hast dich da von der Operator Precedence beißen lassen!? ;)Krishty hat geschrieben:Änder das #if 1 zu #if 0 und prüf, was rauskommt. Vielleicht ist es aber auch bloß spät und ich sehe den Wald vor lauter Bäumen nicht.
&result[8] ist das selbe wie &(result[8]) und gibt die Adresse des achten Elements des result vectors. &result dagegen ist die Adresse des ganzen vectors. (&result) + 8 ist dann nicht die Adresse des achten Elements von results sondern die Adresse des Vektors + 8 * sizeof(result) und das liegt definitiv außerhalb des Buffers -> UB...
- Krishty
- Establishment
- Beiträge: 8240
- Registriert: 26.02.2009, 11:18
- Benutzertext: state is the enemy
- Kontaktdaten:
Re: [C++] Mikrooptimierungs-Log
DAS war der Wald. Ich hatte die ganze Zeit mit Schritten in sizeof result[0] gerechnet statt in sizeof result; danke :)
- Artificial Mind
- Establishment
- Beiträge: 802
- Registriert: 17.12.2007, 17:51
- Wohnort: Aachen
Re: [C++] Mikrooptimierungs-Log
Keine Ahnung ob wir sowas hier schon hatte, aber ich finde es passt hervorragend:
Hochoptimierte Linear-Search vs. Binary-Search
Beinhaltet ASM-level analyse und Späße wie branchless binary search.
Hochoptimierte Linear-Search vs. Binary-Search
Beinhaltet ASM-level analyse und Späße wie branchless binary search.