Hier ist ein Beispiel aus der Praxis: Festpunktmultiplikationen auf alten Compilern.
Diese sind nicht nur für Geräte ohne Gleitkomma nützlich, sie glänzen auch in Bezug auf die Genauigkeit, da sie Ihnen eine Genauigkeit von 32 Bit mit einem vorhersagbaren Fehler bieten (float hat nur 23 Bit und es ist schwieriger, einen Genauigkeitsverlust vorherzusagen). dh gleichmäßige absolute Präzision über den gesamten Bereich anstelle einer nahezu gleichmäßigen relativen Präzision (float
).
Moderne Compiler optimieren dieses Festkomma-Beispiel sehr gut. Weitere moderne Beispiele, die noch compilerspezifischen Code benötigen, finden Sie unter
C hat keinen Vollmultiplikationsoperator (2N-Bit-Ergebnis von N-Bit-Eingängen). Die übliche Art, es in C auszudrücken, besteht darin, die Eingaben in den breiteren Typ umzuwandeln und zu hoffen, dass der Compiler erkennt, dass die oberen Bits der Eingaben nicht interessant sind:
// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
long long a_long = a; // cast to 64 bit.
long long product = a_long * b; // perform multiplication
return (int) (product >> 16); // shift by the fixed point bias
}
Das Problem mit diesem Code ist, dass wir etwas tun, das nicht direkt in der C-Sprache ausgedrückt werden kann. Wir wollen zwei 32-Bit-Zahlen multiplizieren und ein 64-Bit-Ergebnis erhalten, von dem wir das mittlere 32-Bit zurückgeben. In C existiert diese Multiplikation jedoch nicht. Alles, was Sie tun können, ist, die Ganzzahlen auf 64 Bit zu erhöhen und eine 64 * 64 = 64-Multiplikation durchzuführen.
x86 (und ARM, MIPS und andere) können jedoch die Multiplikation in einem einzigen Befehl durchführen. Einige Compiler haben diese Tatsache ignoriert und Code generiert, der eine Laufzeitbibliotheksfunktion aufruft, um die Multiplikation durchzuführen. Die Verschiebung um 16 erfolgt häufig auch durch eine Bibliotheksroutine (auch der x86 kann solche Verschiebungen durchführen).
Wir haben also nur noch ein oder zwei Bibliotheksaufrufe für eine Multiplikation. Dies hat schwerwiegende Folgen. Die Verschiebung ist nicht nur langsamer, die Register müssen über die Funktionsaufrufe hinweg erhalten bleiben, und es hilft auch nicht beim Inlining und Abrollen des Codes.
Wenn Sie denselben Code im (Inline-) Assembler neu schreiben, können Sie einen deutlichen Geschwindigkeitsschub erzielen.
Darüber hinaus ist die Verwendung von ASM nicht der beste Weg, um das Problem zu lösen. Bei den meisten Compilern können Sie einige Assembler-Anweisungen in intrinsischer Form verwenden, wenn Sie sie nicht in C ausdrücken können. Der VS.NET2008-Compiler macht beispielsweise die 32 * 32 = 64-Bit-Mul als __emul und die 64-Bit-Verschiebung als __ll_rshift verfügbar.
Mithilfe von Intrinsics können Sie die Funktion so umschreiben, dass der C-Compiler die Möglichkeit hat, zu verstehen, was vor sich geht. Dies ermöglicht es, den Code einzubinden, das Register zuzuweisen, die Eliminierung gemeinsamer Unterausdrücke durchzuführen und eine konstante Weitergabe durchzuführen. Auf diese Weise erhalten Sie eine enorme Leistungsverbesserung gegenüber dem handgeschriebenen Assembler-Code.
Als Referenz: Das Endergebnis für das Festkomma-Mul für den VS.NET-Compiler lautet:
int inline FixedPointMul (int a, int b)
{
return (int) __ll_rshift(__emul(a,b),16);
}
Der Leistungsunterschied von Festkomma-Teilungen ist noch größer. Ich hatte Verbesserungen bis zu Faktor 10 für den teilungslastigen Fixpunktcode, indem ich ein paar Asm-Zeilen schrieb.
Die Verwendung von Visual C ++ 2013 bietet für beide Möglichkeiten denselben Assemblycode.
gcc4.1 von 2007 optimiert auch die reine C-Version gut. (Im Godbolt-Compiler-Explorer sind keine früheren Versionen von gcc installiert, aber vermutlich könnten sogar ältere GCC-Versionen dies ohne Eigenheiten tun.)
Siehe source + asm für x86 (32-Bit) und ARM im Godbolt-Compiler-Explorer . (Leider gibt es keine Compiler, die alt genug sind, um schlechten Code aus der einfachen reinen C-Version zu erzeugen.)
Moderne CPUs können Dinge tun , C nicht über Operatoren für überhaupt , wie popcnt
oder Bit-Scan den ersten oder letzten Satz Bit zu finden . (POSIX hat eine ffs()
Funktion, aber die Semantik stimmt nicht mit x86 bsf
/ überein bsr
. Siehe https://en.wikipedia.org/wiki/Find_first_set ).
Einige Compiler können manchmal eine Schleife erkennen, die die Anzahl der gesetzten Bits in einer Ganzzahl zählt, und sie zu einem popcnt
Befehl kompilieren (sofern dies zur Kompilierungszeit aktiviert ist). Die Verwendung __builtin_popcnt
in GNU C oder auf x86 ist jedoch viel zuverlässiger, wenn Sie nur sind Targeting-Hardware mit SSE4.2: _mm_popcnt_u32
von<immintrin.h>
.
Oder weisen Sie in C ++ a zu std::bitset<32>
und verwenden Sie .count()
. (Dies ist ein Fall, in dem die Sprache einen Weg gefunden hat, eine optimierte Implementierung von Popcount über die Standardbibliothek portabel verfügbar zu machen, so dass immer eine korrekte Kompilierung möglich ist und alle vom Ziel unterstützten Vorteile genutzt werden können.) Siehe auch https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .
In ähnlicher Weise ntohl
kann auf bswap
(x86 32-Bit-Byte-Swap für Endian-Konvertierung) auf einigen C-Implementierungen, die es haben , kompiliert werden .
Ein weiterer wichtiger Bereich für Intrinsics oder handgeschriebene ASM ist die manuelle Vektorisierung mit SIMD-Anweisungen. Compiler sind nicht schlecht mit einfachen Schleifen wie dst[i] += src[i] * 10.0;
, aber oft schlecht oder gar nicht automatisch vektorisieren, wenn die Dinge komplizierter werden. Zum Beispiel ist es unwahrscheinlich, dass Sie so etwas wie " Atoi mit SIMD implementieren" erhalten. Wird vom Compiler automatisch aus skalarem Code generiert.