Warum verwendet GCC die Multiplikation mit einer seltsamen Zahl bei der Implementierung der ganzzahligen Division?

Lesezeit: 6 Minuten

Benutzeravatar von qiubit
quiubit

Ich habe darüber gelesen div und mul Assembler-Operationen, und ich beschloss, sie in Aktion zu sehen, indem ich ein einfaches Programm in C schrieb:

Dateiaufteilung.c

#include <stdlib.h>
#include <stdio.h>

int main()
{
    size_t i = 9;
    size_t j = i / 5;
    printf("%zu\n",j);
    return 0;
}

Und dann Generieren von Assemblersprachencode mit:

gcc -S division.c -O0 -masm=intel

Aber beim Betrachten generiert division.s Datei enthält sie keine div-Operationen! Stattdessen macht es eine Art schwarze Magie mit Bitverschiebung und magischen Zahlen. Hier ist ein Code-Snippet, das berechnet wird i/5:

mov     rax, QWORD PTR [rbp-16]   ; Move i (=9) to RAX
movabs  rdx, -3689348814741910323 ; Move some magic number to RDX (?)
mul     rdx                       ; Multiply 9 by magic number
mov     rax, rdx                  ; Take only the upper 64 bits of the result
shr     rax, 2                    ; Shift these bits 2 places to the right (?)
mov     QWORD PTR [rbp-8], rax    ; Magically, RAX contains 9/5=1 now, 
                                  ; so we can assign it to j

Was ist denn hier los? Warum verwendet GCC überhaupt kein div? Wie generiert es diese magische Zahl und warum funktioniert alles?

  • gcc optimiert Divisionen durch Konstanten, versuchen Sie Divisionen durch 2,3,4,5,6,7,8 und Sie werden höchstwahrscheinlich sehr unterschiedlichen Code für jeden Fall sehen.

    – Jabberwocky

    16. Dezember 2016 um 12:03 Uhr

  • Hinweis: Magische Zahl -3689348814741910323 konvertiert zu CCCCCCCCCCCCCCCD Als ein uint64_t oder gerade ungefähr (2^64)*4/5.

    – chux – Wiedereinsetzung von Monica

    16. Dezember 2016 um 12:17 Uhr


  • @qiubit: Der Compiler generiert nicht perverserweise ineffizienten Code, nur weil die Optimierung deaktiviert ist. Eine triviale “Optimierung”, die keine Codeumordnung oder Variableneliminierung beinhaltet, wird zum Beispiel trotzdem durchgeführt. Im Wesentlichen wird eine einzelne Quellanweisung in den effizientesten Code für diese isolierte Operation übersetzt. Die Compiler-Optimierung berücksichtigt den umgebenden Code und nicht nur die einzelne Anweisung.

    – Clifford

    16. Dezember 2016 um 12:30 Uhr


  • Lesen Sie diesen großartigen Artikel: Arbeit der Teilung

    – Narr

    16. Dezember 2016 um 12:32 Uhr

  • Einige Compiler tatsächlich Wille Generieren Sie perverserweise ineffizienten Code, weil die Optimierung deaktiviert ist. Sie tun dies insbesondere, um das Debuggen zu vereinfachen, wie z. B. die Möglichkeit, Haltepunkte für einzelne Codezeilen zu setzen. GCC ist in der Tat ziemlich ungewöhnlich, da es keinen echten “Keine Optimierungen”-Modus hat, weil viele seiner Optimierungen konstitutiv eingeschaltet sind. Dies ist ein Beispiel dafür, wo Sie das mit GCC sehen können. Clang hingegen und MSVC, Wille emittieren ein div Unterricht bei -O0. (cc@clifford)

    – Cody Grey

    16. Dezember 2016 um 15:02 Uhr

  • Ich bin mir nicht sicher, ob es fair ist, FP- und Integer-Operationen in einem Geschwindigkeitsvergleich zusammenzufassen, @fuz. Vielleicht sollte Sneftel das sagen Aufteilung ist am langsamsten ganze Zahl Operation, die Sie auf einem modernen Prozessor durchführen können? Außerdem wurden einige Links zu weiteren Erklärungen dieser “Magie” in Kommentaren bereitgestellt. Denken Sie, dass es angemessen wäre, sie in Ihrer Antwort für die Sichtbarkeit zu sammeln? 1, 2, 3

    – Cody Grey

    16. Dezember 2016 um 15:00 Uhr

  • Denn der Arbeitsablauf ist funktional identisch … dies ist immer eine Voraussetzung, auch bei -O3. Der Compiler muss Code erstellen, der korrekte Ergebnisse für alle möglichen Eingabewerte liefert. Dies ändert sich nur für Gleitkommazahlen mit -ffast-math, und AFAIK gibt es keine “gefährlichen” ganzzahligen Optimierungen. (Bei aktivierter Optimierung kann der Compiler möglicherweise etwas über den möglichen Wertebereich beweisen, wodurch er etwas verwenden kann, das beispielsweise nur für nicht negative vorzeichenbehaftete Ganzzahlen funktioniert.)

    – Peter Cordes

    16. Dezember 2016 um 15:55 Uhr

  • Die wahre Antwort ist, dass gcc -O0 Code immer noch durch interne Repräsentationen umwandelt, um C in Maschinencode umzuwandeln. Es kommt einfach vor, dass modulare multiplikative Inverse sogar bei standardmäßig aktiviert sind -O0 (aber nicht mit -Os). Andere Compiler (wie clang) verwenden DIV für Nicht-Potenz-von-2-Konstanten at -O0. verwandt: Ich glaube, ich habe einen Absatz darüber in meine handschriftliche Asm-Antwort zur Collatz-Vermutung aufgenommen

    – Peter Cordes

    16. Dezember 2016 um 16:00 Uhr

  • @PeterCordes Und ja, ich denke, GCC (und viele andere Compiler) haben vergessen, eine gute Begründung dafür zu finden, “welche Art von Optimierungen gelten, wenn die Optimierung deaktiviert ist”. Nachdem ich den größten Teil des Tages damit verbracht habe, einen obskuren Codegen-Fehler aufzuspüren, bin ich im Moment etwas verärgert darüber.

    – Sneftel

    16. Dezember 2016 um 20:19 Uhr

  • @Sneftel: Das liegt wahrscheinlich nur an der Anzahl der Anwendungsentwickler, die aktiv sind beschweren an die Compiler-Entwickler darüber, dass ihr Code schneller als erwartet ausgeführt wird, ist relativ gering.

    – dan04

    16. Dezember 2016 um 23:37 Uhr

  • @ user2357112 Sie können gerne Ihre eigene Antwort posten, aber ich stimme nicht zu. Sie können sich die Multiplikation als eine 64,0-Bit-mal-0,64-Bit-Multiplikation vorstellen, die eine 128-Bit-Festkommaantwort ergibt, von der die niedrigsten 64 Bits verworfen werden, dann eine Division durch 4 (wie ich im ersten Absatz anmerke). Möglicherweise können Sie sich eine alternative modulare arithmetische Antwort einfallen lassen, die die Bitbewegungen gleichermaßen gut erklärt, aber ich bin mir ziemlich sicher, dass dies als Erklärung funktioniert.

    – Licht

    16. Dezember 2016 um 18:36 Uhr


  • Der Wert ist eigentlich “CCCCCCCCCCCCCCCD”. Das letzte D ist wichtig, es stellt sicher, dass beim Abschneiden des Ergebnisses exakte Divisionen die richtige Antwort ergeben.

    – Plugwash

    16. Dezember 2016 um 18:44 Uhr

  • Macht nichts. Ich habe nicht gesehen, dass sie die oberen 64 Bit des 128-Bit-Multiplikationsergebnisses nehmen; In den meisten Sprachen ist das nicht möglich, daher war mir zunächst nicht klar, dass es passiert. Diese Antwort würde durch eine ausdrückliche Erwähnung erheblich verbessert, dass das Nehmen der oberen 64 Bit des 128-Bit-Ergebnisses der Multiplikation mit einer Festkommazahl und dem Abrunden entspricht. (Außerdem wäre es gut zu erklären, warum es 4/5 statt 1/5 sein muss und warum wir 4/5 aufrunden statt abrunden müssen.)

    – Benutzer2357112

    16. Dezember 2016 um 18:46 Uhr

  • Afaict Sie müssten ausrechnen, wie groß ein Fehler sein muss, um eine Division durch 5 nach oben über eine Rundungsgrenze zu werfen, und das dann mit dem schlimmsten Fehler in Ihrer Berechnung vergleichen. Vermutlich haben die gcc-Entwickler dies getan und sind zu dem Schluss gekommen, dass es immer die richtigen Ergebnisse liefern wird.

    – Plugwash

    16. Dezember 2016 um 19:12 Uhr

  • Eigentlich müssen Sie wahrscheinlich nur die 5 höchstmöglichen Eingabewerte überprüfen, wenn diese korrekt runden, sollte auch alles andere passen.

    – Plugwash

    16. Dezember 2016 um 19:15 Uhr

  • Diese Antwort verwandelte modulare multiplikative Inverse von “Mathematik, die komplizierter aussieht, als ich mir die Zeit nehmen möchte” in etwas Sinnvolles. +1 für die leicht verständliche Version. Ich musste nie etwas anderes tun, als nur vom Compiler generierte Konstanten zu verwenden, also habe ich andere Artikel, die die Mathematik erklären, nur überflogen.

    – Peter Cordes

    17. Dezember 2016 um 4:08 Uhr


  • Ich sehe im Code überhaupt nichts mit modularer Arithmetik zu tun. Keine Ahnung, woher andere Kommentatoren das nehmen.

    – Plugwash

    17. Dezember 2016 um 13:05 Uhr

  • Es ist modulo 2^n, wie alle ganzzahligen Mathematik in einem Register. de.wikipedia.org/wiki/…

    – Peter Cordes

    17. Dezember 2016 um 16:49 Uhr


  • @PeterCordes modulare multiplikative Inverse werden für die exakte Division verwendet, afaik, sie sind für die allgemeine Division nicht nützlich

    – Harald

    17. Dezember 2016 um 23:16 Uhr

  • @PeterCordes Multiplikation mit Festkomma-Kehrwert? Ich weiß nicht, wie jeder es nennt, aber ich würde es wahrscheinlich so nennen, es ist ziemlich beschreibend

    – Harald

    18. Dezember 2016 um 21:06 Uhr

1426350cookie-checkWarum verwendet GCC die Multiplikation mit einer seltsamen Zahl bei der Implementierung der ganzzahligen Division?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy