Warum generiert GCC eine so radikal unterschiedliche Assembly für fast denselben C-Code?

Lesezeit: 4 Minuten

Benutzeravatar von orlp
Orlp

Beim Schreiben einer optimierten ftol Funktion habe ich ein sehr merkwürdiges Verhalten gefunden GCC 4.6.1. Lassen Sie mich Ihnen zuerst den Code zeigen (zur Verdeutlichung habe ich die Unterschiede markiert):

fast_trunc_one, C:

int fast_trunc_one(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = mantissa << -exponent;                       /* diff */
    } else {
        r = mantissa >> exponent;                        /* diff */
    }

    return (r ^ -sign) + sign;                           /* diff */
}

fast_trunc_two, C:

int fast_trunc_two(int i) {
    int mantissa, exponent, sign, r;

    mantissa = (i & 0x07fffff) | 0x800000;
    exponent = 150 - ((i >> 23) & 0xff);
    sign = i & 0x80000000;

    if (exponent < 0) {
        r = (mantissa << -exponent) ^ -sign;             /* diff */
    } else {
        r = (mantissa >> exponent) ^ -sign;              /* diff */
    }

    return r + sign;                                     /* diff */
}

Scheint gleich richtig? Nun, GCC ist anderer Meinung. Nach dem Kompilieren mit gcc -O3 -S -Wall -o test.s test.c Dies ist die Assembly-Ausgabe:

fast_trunc_one, generiert:

_fast_trunc_one:
LFB0:
    .cfi_startproc
    movl    4(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %edx
    andl    $8388607, %edx
    sarl    $23, %eax
    orl $8388608, %edx
    andl    $255, %eax
    subl    %eax, %ecx
    movl    %edx, %eax
    sarl    %cl, %eax
    testl   %ecx, %ecx
    js  L5
    rep
    ret
    .p2align 4,,7
L5:
    negl    %ecx
    movl    %edx, %eax
    sall    %cl, %eax
    ret
    .cfi_endproc

fast_trunc_two, generiert:

_fast_trunc_two:
LFB1:
    .cfi_startproc
    pushl   %ebx
    .cfi_def_cfa_offset 8
    .cfi_offset 3, -8
    movl    8(%esp), %eax
    movl    $150, %ecx
    movl    %eax, %ebx
    movl    %eax, %edx
    sarl    $23, %ebx
    andl    $8388607, %edx
    andl    $255, %ebx
    orl $8388608, %edx
    andl    $-2147483648, %eax
    subl    %ebx, %ecx
    js  L9
    sarl    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_remember_state
    .cfi_def_cfa_offset 4
    .cfi_restore 3
    ret
    .p2align 4,,7
L9:
    .cfi_restore_state
    negl    %ecx
    sall    %cl, %edx
    movl    %eax, %ecx
    negl    %ecx
    xorl    %ecx, %edx
    addl    %edx, %eax
    popl    %ebx
    .cfi_restore 3
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc

Das ist ein extrem Unterschied. Dies wird tatsächlich auch im Profil angezeigt, fast_trunc_one ist etwa 30% schneller als fast_trunc_two. Nun meine Frage: woran liegt das?

  • Zu Testzwecken habe ich ein Gist erstellt hier wo Sie den Quellcode einfach kopieren/einfügen und sehen können, ob Sie den Fehler auf anderen Systemen/Versionen von GCC reproduzieren können.

    – orlp

    20. April 2012 um 17:16 Uhr

  • Legen Sie die Testfälle in einem eigenen Verzeichnis ab. Kompilieren Sie sie mit -S -O3 -da -fdump-tree-all. Dadurch werden viele Momentaufnahmen der Zwischendarstellung erstellt. Gehen Sie sie nebeneinander durch (sie sind nummeriert), und Sie sollten in der Lage sein, die fehlende Optimierung im ersten Fall zu finden.

    – zol

    20. April 2012 um 17:21 Uhr

  • Vorschlag zwei: Alle ändern int zu unsigned int und sehen, ob der Unterschied verschwindet.

    – zol

    20. April 2012 um 17:22 Uhr

  • Die beiden Funktionen scheinen etwas unterschiedliche Berechnungen durchzuführen. Während die Ergebnisse dieselben sein können, ist der Ausdruck (r + shifted) ^ sign ist nicht dasselbe wie r + (shifted ^ sign). Ich denke, das verwirrt den Optimierer? FWIW, MSVC 2010 (16.00.40219.01) erzeugt Listen, die nahezu identisch sind: gist.github.com/2430454

    – DCoder

    20. April 2012 um 17:26 Uhr

  • @DCoder: Oh verdammt! Das habe ich nicht bemerkt. Es ist jedoch nicht die Erklärung für den Unterschied. Lassen Sie mich die Frage mit einer neuen Version aktualisieren, in der dies ausgeschlossen ist.

    – orlp

    20. April 2012 um 17:36 Uhr

  • Bearbeiten, es sieht so aus, als hätte ich Revision zwei beantwortet. Die aktuelle Revision hat die beiden Beispiele umgedreht und den Code ein wenig geändert … das ist verwirrend.

    – Mystisch

    20. April 2012 um 18:03 Uhr

  • @nightcracker Keine Sorge. Ich habe meine Antwort aktualisiert, um sie mit der aktuellen Version zu synchronisieren.

    – Mystisch

    20. April 2012 um 18:17 Uhr

  • @Mystcial: Ihre letzte Aussage stimmt mit der neuen Version nicht mehr, wodurch Ihre Antwort ungültig wird (sie beantwortet nicht die wichtigste Frage, “Warum generiert GCC eine so radikal andere Baugruppe?”.)

    – orlp

    20. April 2012 um 18:24 Uhr


  • Antwort erneut aktualisiert. Ich bin mir nicht sicher, ob es befriedigend genug ist. Aber ich glaube nicht, dass ich es viel besser machen kann, ohne genau zu wissen, wie die relevanten GCC-Optimierungsdurchläufe funktionieren.

    – Mystisch

    20. April 2012 um 18:43 Uhr


  • @Mystcial: Genau genommen, solange der vorzeichenbehaftete Typ in diesem Code fälschlicherweise verwendet wird, sind so ziemlich alle Transformationen, die der Compiler hier vornimmt, in Fällen, in denen das Verhalten nicht definiert ist …

    – R.. GitHub HÖR AUF, EIS ZU HELFEN

    9. Juni 2014 um 18:24 Uhr

  • Warum hast du deine Antwort zerstört? Oded schien der Bearbeitung ebenfalls nicht zuzustimmen ;-).

    – Peter – Setzen Sie Monica wieder ein

    14. November 2016 um 15:10 Uhr


  • @PeterA.Schneider Alle seine Antworten scheinen am selben Tag zerstört worden zu sein. Ich denke, jemand mit seinen (gestohlenen?) Kontodaten hat es getan.

    – Trinity420

    8. Dezember 2018 um 23:54 Uhr

  • Ist der Compiler, den Sie nicht nennen wollen, ein streng geheimer Supercompiler?

    – orlp

    30. April 2012 um 12:02 Uhr

  • der Top-Secret-Compiler ist wahrscheinlich Intel icc. Ich habe nur die 32-Bit-Variante, aber sie erzeugt Code, der diesem sehr ähnlich ist.

    – Janus Troelsen

    1. Mai 2012 um 21:43 Uhr

  • Ich glaube auch, dass es ICC ist. Der Compiler weiß, dass der Prozessor Parallelität auf Befehlsebene ausführen kann und somit beide Zweige gleichzeitig berechnet werden können. Der Overhead einer bedingten Bewegung ist viel geringer als der Overhead einer falschen Verzweigungsvorhersage.

    – Philipp Navara

    19. Mai 2012 um 9:15 Uhr

1425540cookie-checkWarum generiert GCC eine so radikal unterschiedliche Assembly für fast denselben C-Code?

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

Privacy policy