x86_64: Warum ist uint_least16_t schneller als uint_fast16_t (für Multiplikation)

8

Der C-Standard ist bezüglich der Typenfamilie uint_fast * _t ziemlich unklar. Auf einem System gcc-4.4.4 linux x86_64 sind die Typen uint_fast16_t und uint_fast32_t beide 8 Bytes groß. Die Multiplikation von 8-Byte-Zahlen scheint jedoch ziemlich langsamer zu sein als die Multiplikation von 4-Byte-Zahlen. Der folgende Codeabschnitt zeigt Folgendes:

%Vor%

Wenn ich den Befehl time für das Programm ausführe, bekomme ich

%Vor%

Wenn ich den Typ in uint_fast16_t (und den printf-Modifizierer) ändere, wird das Timing zu

%Vor%

Wäre es also nicht viel besser, wenn der Header stdint.h uint_fast16_t (und auch uint_fast32_t) als 4-Byte-Typ definiert wäre?

    
Luís Fernando Schultz Xavier 07.11.2010, 02:48
quelle

5 Antworten

4

AFAIK-Compiler definieren nur ihre eigenen Versionen von (u)int_(fast/least)XX_t -Typen, wenn diese nicht bereits vom System definiert sind. Das liegt daran, dass es sehr wichtig ist, dass diese Typen in allen Bibliotheken / Binärdateien auf einem einzigen System gleich definiert sind. Andernfalls, wenn andere Compiler diese Typen anders definieren würden, könnte eine mit CompilerA erstellte Bibliothek einen anderen uint_fast32_t -Typ haben als eine mit CompilerB erstellte Binärdatei, aber diese Binärdatei kann immer noch mit der Bibliothek verlinken; Es gibt keine

Standardvorschrift , dass der gesamte ausführbare Code eines Systems vom selben Compiler erstellt werden muss (tatsächlich ist es auf manchen Systemen, zB Windows, üblich, dass Code von allen Arten kompiliert wurde) verschiedene Compiler). Wenn nun diese Binärdatei eine Funktion der Bibliothek aufruft, werden Dinge brechen!

Die Frage ist also: Ist es wirklich GCC, uint_fast16_t hier zu definieren, oder ist es tatsächlich Linux (ich meine den Kernel hier) oder vielleicht sogar die Standard C Lib (glibc in den meisten Fällen), die diese Typen definiert? Wenn Linux oder glibc diese definieren, hat GCC, das auf diesem System aufgebaut ist, keine andere Wahl, als die Konventionen zu übernehmen, die diese festgelegt haben. Dies gilt auch für alle anderen Typen von Variablenbreiten: char , short , int , long , long long ; all diese Typen haben nur eine minimale garantierte Bitbreite im C Standard (und für int sind es tatsächlich 16 Bit, also auf Plattformen, wo int 32 Bit ist, ist es schon viel größer als würde von der Norm verlangt werden).

Abgesehen davon frage ich mich eigentlich, was mit Ihrer CPU / Ihrem Compiler / System nicht stimmt. Auf meinem System ist die 64-Bit-Multiplikation genauso schnell wie die 32-Bit-Multiplikation. Ich habe Ihren Code geändert, um 16, 32 und 64 Bit zu testen:

%Vor%

Wenn ich nicht optimierten Code (-O0) verwende, bekomme ich:

%Vor%

Mit optimiertem Code (-O3) bekomme ich:

%Vor%

Der zweite Ausgang ist ziemlich interessant. @R .. schrieb in einem Kommentar oben:

  

Auf x86_64 sollte 32-Bit-Arithmetik niemals langsamer als 64-Bit sein   Arithmetik, Punkt.

Die zweite Ausgabe zeigt, dass dasselbe für 32/16 Bit-Arithmetik nicht gesagt werden kann. 16-Bit-Arithmetik kann auf einer 32/64 Bit-CPU wesentlich langsamer sein, obwohl meine x86-CPU nativ 16-Bit-Arithmetik ausführen kann; im Gegensatz zu einigen anderen CPUs, wie zum Beispiel einem PPC, der nur 32-Bit-Arithmetik ausführen kann. Dies scheint jedoch nur für die Multiplikation auf meiner CPU zu gelten, wenn der Code für Addition / Subtraktion / Division geändert wird, gibt es keinen signifikanten Unterschied zwischen 16 und 32 Bit mehr.

Die obigen Ergebnisse stammen von einem Intel Core i7 (2,66 GHz), aber wenn jemand interessiert ist, kann ich diesen Benchmark auch auf einem Intel Core 2 Duo (eine CPU-Generation älter) und auf einem Motorola PowerPC G4 ausführen. p>     

Mecki 28.08.2012 11:39
quelle
3

Ich denke, dass eine solche Designentscheidung nicht einfach zu treffen ist. Es hängt von vielen Faktoren ab. Für den Moment nehme ich Ihr Experiment nicht als schlüssig, siehe unten.

Zunächst einmal gibt es kein one einzelnes Konzept dessen, was fast bedeuten sollte. Hier betonten Sie die Multiplikation an Ort und Stelle, die nur ein bestimmter Gesichtspunkt ist.

Dann ist x86_64 eine Architektur und kein Prozessor. Daher könnten die Ergebnisse für die verschiedenen Prozessoren in dieser Familie sehr unterschiedlich sein. Ich glaube nicht, dass es vernünftig wäre, wenn gcc die Art der Entscheidung von bestimmten Befehlszeilenschaltern abhängig machen würde, die für einen bestimmten Prozessor optimiert sind.

Kommen Sie nun zu Ihrem Beispiel zurück. Ich nehme an, Sie haben sich auch den Assembler Code angeschaut? Hat es beispielsweise SSE-Anweisungen verwendet, um Ihren Code zu realisieren? Hast du prozessorspezifische Optionen aktiviert, etwa -march=native ?

Bearbeiten: Ich habe ein wenig mit Ihrem Testprogramm experimentiert und wenn ich es genau so belasse, wie es ist, kann ich Ihre Messungen grundsätzlich reproduzieren. Aber ich modifiziere und spiele damit herum und bin noch weniger davon überzeugt, dass es schlüssig ist.

Wenn z. B. die innere Schleife geändert wird, um nach unten zu gehen, sieht der Assembler fast genauso aus wie vorher (aber mit Dekrement und einem Test gegen 0), aber die Ausführung dauert ungefähr 50% mehr. Ich denke also, das Timing hängt sehr stark von der Umgebung der Instruktion ab, die Sie benchmarken wollen, Pipeline-Stände, was auch immer. Sie müssten Bankcodes sehr unterschiedlicher Art erstellen, wo die Anweisungen in verschiedenen Kontexten ausgegeben werden, Ausrichtungsprobleme und Vektorisierung spielen, um eine Entscheidung zu treffen, welche die geeigneten Typen für die fast typedef s sind.

    
Jens Gustedt 07.11.2010 07:44
quelle
2

Die tatsächliche Leistung zur Laufzeit ist ein sehr kompliziertes Thema. Mit vielen Faktoren von RAM-Speicher, Festplatten, OS'es; Und die vielen prozessorspezifischen Macken. Aber das wird dir einen schweren Schlag versetzen:

N_fastX_t

  • Optimale Größe, um die meisten Operationen (Addition und Subtraktion) effizient für den Prozessor zu berechnen. Dies ist hardwarespezifisch, wobei 32-Bit-Variablen nativ und schneller als 16-Bit-Variablen sind (und somit verwendet werden). (Zum Beispiel)
  • Da es in Bezug auf Cacheline-Treffer nicht so gut wie N_leastX ist, sollte dies hauptsächlich dann verwendet werden, wenn nur eine einzelne Variable so schnell wie möglich benötigt wird. Obwohl es nicht in einem großen Array ist (wie groß ist es optimal zwischen beiden zu wechseln ist leider plattformabhängig)
  • Beachten Sie, dass Fast vs mindestens einige Macken hat, hauptsächlich Multiplikation und Divisionen. Das ist plattformspezifisch. Wenn jedoch die meisten Operationen Additionen / Subtraktionen / oder / und sind. Es ist im Allgemeinen sicher anzunehmen, dass es schneller ist. (noch einmal beachten Sie den CPU-Cache und andere Eigenarten)

N_leastX_t

  • Die kleinste Variable, die die Hardware zulässt, also mindestens die X-Größe. (Einige Plattformen können beispielsweise keine Variablen unterhalb von 4 Bytes zuweisen. Tatsächlich nehmen die meisten Ihrer BOOL-Variablen mindestens ein Byte und kein Bit ein)
  • Kann über eine kostspielige CPU-Software-Emulation berechnet werden, wenn keine Hardware-Unterstützung vorhanden ist.
  • Kann Leistungseinbußen aufgrund von teilweiser Hardwareunterstützung (im Vergleich zu schnell) auf einer einzigen Basis haben.
  • JEDOCH : Da weniger Platz benötigt wird, kann es die Cache-Zeilen häufiger treffen. Dies ist in Arrays viel stärker ausgeprägt. Und als solche wird schneller (Speicherkosten & gt; CPU-Kosten) Siehe Ссылка für weitere Details.

Das Multiplikationsproblem?

Auch um zu beantworten, warum die größere fastX-Variable in der Multiplikation langsamer wäre. Ursache ist aufgrund der Natur der Multiplikation. (ähnlich dem, was man in der Schule dachte)

Ссылка

%Vor%

Es ist jedoch wichtig zu wissen, dass ein Computer ein "logischer Idiot" ist. Während es für uns Menschen offensichtlich ist, den abschließenden Nullenschritt zu überspringen. Der Computer wird es immer noch herausfinden (es ist billiger, als es konditional zu überprüfen und dann trotzdem zu arbeiten). Daraus ergibt sich eine Eigenart für eine größere Variable mit dem gleichen Wert

%Vor%

Während ich die restlichen 0x0-Zusätze im Multiplikationsprozess nicht spammte. Es ist wichtig zu beachten, dass der Computer "sie erledigt". Und daher ist es natürlich, dass eine größere variable Multiplikation längere Zeit braucht als ihr kleineres Gegenstück. (Daher ist es immer gut, Multiplikationen und Divisionen zu vermeiden, wann immer es möglich ist).

Aber hier kommt die zweite Eigenart. Es kann nicht für alle Prozessoren gelten. Es ist wichtig zu beachten, dass alle CPU-Operationen in CPU-Zyklen gezählt werden. In dem in jedem Zyklus Dutzende (oder mehr) solcher kleinen Additionsoperationen durchgeführt werden, wie oben gesehen. Als Ergebnis kann eine 8-Bit-Addition die gleiche Zeit wie eine 8-Bit-Multiplikation usw. annehmen. Aufgrund der verschiedenen Optimierungen und CPU-spezifischen Macken.

Wenn es dich so betrifft. Gehen Sie zu Intel: Ссылка

Zusätzliche Erwähnung von CPU vs RAM

Da die CPU nach Moore's Gesetz mehrere Male schneller ist als Ihr DDR3 RAM.

Dies kann zu Situationen führen, in denen mehr Zeit damit verbracht wird, die Variable vom RAM nachzuschauen und dann von der CPU "zu berechnen". Dies ist in langen Zeigerketten am ausgeprägtesten.

Also, während auf dem meisten Prozessor ein CPU-Cache existiert, um die "RAM Look-Up" -Zeit zu reduzieren. Seine Verwendung ist auf bestimmte Fälle beschränkt (wobei die Cache-Zeile am meisten profitiert). Und für Fälle, in denen es nicht passt. Beachten Sie, dass die RAM-Nachschlagzeit & gt; CPU-Verarbeitungszeit (ohne Multiplikation / Divisionen / einige Macken)

    
PicoCreator 15.05.2013 06:37
quelle
1

Nur weil ich neugierig auf die schnellen Integer-Typen war, habe ich einen echten Parser getestet, der in seinem semantischen Teil einen ganzzahligen Typ für die Indexierung von Arrays und C ++ - Containern verwendet hat. Es führte eine Mischung von Operationen statt einer einfachen Schleife durch und das meiste Programm war nicht von dem gewählten Integertyp abhängig. Tatsächlich wäre für meine speziellen Daten jeder Integer-Typ in Ordnung gewesen. So haben alle Versionen die gleiche Ausgabe produziert.

Auf Assemblerebene gibt es 8 Fälle: vier für die Größen und 2 für die Signiertheit. Die 24 ISO C-Typnamen müssen den acht Grundtypen zugeordnet werden. Wie Jens bereits feststellte, muss ein "gutes" Mapping den jeweiligen Prozessor und den jeweiligen Code berücksichtigen. Daher sollten wir in der Praxis keine perfekten Ergebnisse erwarten, obwohl die Compilerschreiber den generierten Code kennen sollten.

Viele Läufe des Beispiels wurden gemittelt, so dass der Fluktuationsbereich der Laufzeit nur etwa 2 der kleinsten gegebenen Ziffer ist. Für diese spezielle Konfiguration waren die Ergebnisse:

  • Keine Laufzeitdifferenz zwischen int16_t / uint16_t bzw. int64_t / uint64_t .
  • Nicht signierte Versionen sind viel schneller für int8_t / uint8_t bzw. int32_t / uint32_t .
  • Unsignierte Versionen sind immer kleiner (Text und Datensegment) als signierte Versionen.

Compiler: g ++ 4.9.1, Optionen: -O3 mtune = generic -march = x86-64

CPU: Intel ™ Kern ™ 2 Duo E8400 @ 3.00GHz

Die Zuordnung

%Vor%

Größen und Timings

%Vor%     
hermannk 05.02.2015 21:03
quelle
0

Ja, ich denke, das ist einfach ein Fehler. Leider kann man nicht einfach so Fehler beheben, ohne die ABI zu brechen, aber das spielt keine Rolle, da praktisch niemand (und sicherlich keine Bibliotheksfunktionen, die ich kenne) tatsächlich die *int_fast*_t -Typen verwendet.

    
R.. 07.11.2010 03:03
quelle

Tags und Links