gcc -O0 optimiert weiterhin “unbenutzten” Code. Gibt es ein Compiler-Flag, um das zu ändern?

Lesezeit: 9 Minuten

Benutzeravatar von BurnsBA
BurnsBA

Wie ich in dieser Frage angesprochen habe, entfernt gcc (ja, mit -O0) eine Codezeile _mm_div_ss(s1, s2); vermutlich weil das Ergebnis nicht gespeichert wird. Dies jedoch sollte löst eine Fließkomma-Ausnahme aus und löst SIGFPE aus, was nicht passieren kann, wenn der Aufruf entfernt wird.

Frage: Gibt es ein Flag oder mehrere Flags, die an gcc übergeben werden, damit der Code so kompiliert wird, wie er ist? Ich denke so etwas wie fno-remove-unused aber sowas sehe ich nicht. Idealerweise wäre dies ein Compiler-Flag, anstatt meinen Quellcode ändern zu müssen, aber wenn dies nicht unterstützt wird, gibt es stattdessen ein gcc-Attribut/Pragma?

Dinge, die ich versucht habe:

$ gcc --help=optimizers | grep -i remove

keine Ergebnisse.

$ gcc --help=optimizers | grep -i unused

keine Ergebnisse.

Und alle toten Code-/Eliminierungs-Flags explizit deaktivieren – beachten Sie, dass es keine Warnung über unbenutzten Code gibt:

$ gcc -O0 -msse2 -Wall -Wextra -pedantic -Winline \
     -fno-dce -fno-dse -fno-tree-dce \
     -fno-tree-dse -fno-tree-fre -fno-compare-elim -fno-gcse  \
     -fno-gcse-after-reload -fno-gcse-las -fno-rerun-cse-after-loop \
     -fno-tree-builtin-call-dce -fno-tree-cselim a.c
a.c: In function ‘main’:
a.c:25:5: warning: ISO C90 forbids mixed declarations and code [-Wpedantic]
     __m128 s1, s2;
     ^
$

Quellprogramm

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <xmmintrin.h>

static void sigaction_sfpe(int signal, siginfo_t *si, void *arg)
{
    printf("%d,%d,%d\n", signal, si!=NULL?1:0, arg!=NULL?1:0);
    printf("inside SIGFPE handler\nexit now.\n");
    exit(1);
}

int main()
{
    struct sigaction sa;

    memset(&sa, 0, sizeof(sa));
    sigemptyset(&sa.sa_mask);
    sa.sa_sigaction = sigaction_sfpe;
    sa.sa_flags = SA_SIGINFO;
    sigaction(SIGFPE, &sa, NULL);

    _mm_setcsr(0x00001D80);

    __m128 s1, s2;
    s1 = _mm_set_ps(1.0, 1.0, 1.0, 1.0);
    s2 = _mm_set_ps(0.0, 0.0, 0.0, 0.0);
    _mm_div_ss(s1, s2);

    printf("done (no error).\n");

    return 0;
}

Das Kompilieren des obigen Programms ergibt

$ ./a.out
done (no error).

Ändern der Linie

_mm_div_ss(s1, s2);

zu

s2 = _mm_div_ss(s1, s2); // add "s2 = "

liefert das erwartete Ergebnis:

$ ./a.out
inside SIGFPE handler

Bearbeiten Sie mit mehr Details.

Dies scheint im Zusammenhang mit der __always_inline__ Attribut auf der _mm_div_ss Definition.

$ cat t.c
int
div(int b)
{
    return 1/b;
}

int main()
{
    div(0);
    return 0;
}


$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$  

(keine Warnungen oder Fehler)

$ ./t.out
Floating point exception
$

vs unten (gleich außer für Funktionsattribute)

$ cat t.c
__inline int __attribute__((__always_inline__))
div(int b)
{
    return 1/b;
}

int main()
{
    div(0);
    return 0;
}

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
$   

(keine Warnungen oder Fehler)

$ ./t.out
$

Hinzufügen des Funktionsattributs __warn_unused_result__ gibt zumindest eine hilfreiche Nachricht:

$ gcc -O0 -Wall -Wextra -pedantic -Winline t.c -o t.out
t.c: In function ‘main’:
t.c:9:5: warning: ignoring return value of ‘div’, declared with attribute warn_unused_result [-Wunused-result]
     div(0);
     ^

bearbeiten:

Einige Diskussionen über die gcc-Mailingliste. Letztendlich denke ich, dass alles wie beabsichtigt funktioniert.

  • Versuchen Sie es mit __attribute__((used)) mit den beteiligten Variablen.

    – Eugen Sch.

    3. Oktober 2016 um 13:44 Uhr

  • Vielleicht hilft es, s1 und s2 als flüchtig zu deklarieren …

    – Malkocoglu

    3. Oktober 2016 um 13:47 Uhr

Warum gibt gcc die angegebene Anweisung nicht aus?

Ein Compiler erzeugt Code, der die haben muss beobachtbares Verhalten von der Norm vorgegeben. Alles, was nicht beobachtbar ist, kann nach Belieben geändert (und optimiert) werden, da es das Verhalten des Programms (wie angegeben) nicht ändert.

Wie können Sie es in Unterwerfung schlagen?

Der Trick besteht darin, den Compiler glauben zu machen, dass das Verhalten des bestimmten Codeabschnitts tatsächlich beobachtbar ist.

Da dies ein häufig bei Mikro-Benchmarks auftretendes Problem ist, empfehle ich Ihnen, sich anzusehen, wie (zum Beispiel) Google-Benchmark damit umgeht. Aus benchmark_api.h wir bekommen:

template <class Tp>
inline void DoNotOptimize(Tp const& value) {
    asm volatile("" : : "g"(value) : "memory");
}

Die Einzelheiten von diese Syntax sind langweilig, für unseren Zweck brauchen wir nur zu wissen:

  • "g"(value) erzählt das value wird als Eingabe für die Anweisung verwendet
  • "memory" ist eine Lese-/Schreibbarriere zur Kompilierzeit

Also können wir den Code ändern in:

asm volatile("" : : : "memory");

__m128 result = _mm_div_ss(s1, s2);

asm volatile("" : : "g"(result) : );

Die:

  • zwingt den Compiler, dies zu berücksichtigen s1 und s2 können zwischen ihrer Initialisierung und Verwendung geändert worden sein
  • zwingt den Compiler zu berücksichtigen, dass das Ergebnis der Operation verwendet wird

Es ist kein Flag erforderlich, und es sollte auf jeder Optimierungsebene funktionieren (ich habe es auf getestet https://gcc.godbolt.org/ bei -O3).

  • Haben Sie eine Quelle dafür, was gcc als beobachtetes Verhalten entscheidet oder nicht? Ich versuche einzugrenzen, ob das Entfernen dieser Anweisung ohne Vorwarnung ein Fehler, unbeabsichtigt oder beabsichtigt ist.

    – BurnsBA

    4. Oktober 2016 um 19:19 Uhr

  • @BurnsBA: Der C++-Standard ist die Referenz dafür, was beobachtbares Verhalten ist oder nicht. Es ist auch ziemlich schwer zu lesen und voller Eckfälle … Im Allgemeinen ist beobachtbares Verhalten für Singlethread-Programme alles, was die Ausgabe (E / A) von Programmen beeinflusst. Bei Multithreading-Programmen wird es komplizierter, da viele Interleaving-Ausgaben möglich sind.

    – Matthias M.

    5. Oktober 2016 um 6:45 Uhr

  • Das DoNotOptimize Implementierung ist einer der hinterhältigsten Tricks, die ich seit langem gesehen habe, lang Zeit. Wow.

    – Qix – MONICA WURDE MISSHANDELT

    7. Oktober 2020 um 23:39 Uhr

GCC “optimiert” hier nichts heraus. Es generiert einfach keinen nutzlosen Code. Es scheint eine weit verbreitete Illusion zu sein, dass es eine reine Form von Code gibt, die der Compiler generieren sollte, und alle Änderungen daran sind eine “Optimierung”. Es gibt keine solche Sache.

Der Compiler erstellt eine Datenstruktur, die darstellt, was der Code bedeutet, wendet dann einige Transformationen auf diese Datenstruktur an und generiert daraus Assembler, der dann zu Anweisungen kompiliert wird. Wenn Sie ohne “Optimierungen” kompilieren, bedeutet dies nur, dass der Compiler nur den geringstmöglichen Aufwand zum Generieren von Code aufwenden wird.

In diesem Fall ist die gesamte Anweisung nutzlos, da sie nichts bewirkt und sofort weggeworfen wird (nach dem Erweitern der Inlines und der Bedeutung der Builtins entspricht sie dem Schreiben a/b;der Unterschied ist, dass das Schreiben a/b; wird eine Warnung ausgeben statement with no effect während die Builtins wahrscheinlich nicht von denselben Warnungen behandelt werden). Dies ist keine Optimierung, der Compiler müsste tatsächlich zusätzliche Anstrengungen unternehmen, um eine Bedeutung für eine bedeutungslose Anweisung zu erfinden, dann eine temporäre Variable vortäuschen, um das Ergebnis dieser Anweisung zu speichern, um sie dann wegzuwerfen.

Was Sie suchen, sind nicht Flags zum Deaktivieren von Optimierungen, sondern Pessimisierungs-Flags. Ich glaube nicht, dass Compiler-Entwickler Zeit damit verschwenden, solche Flags zu implementieren. Außer vielleicht als Aprilscherz.

  • Wie zeige ich Warnungen zu Aussagen ohne Wirkung an? Da -Wall -Wextra -pedantic zeigt nichts an.

    – BurnsBA

    3. Oktober 2016 um 14:45 Uhr

  • @BurnsBA Ich konnte es auch nicht dazu bringen, eine Warnung zu erzeugen. Das ist wahrscheinlich ein Fehler in gcc. Senden Sie ihnen einen Fehlerbericht.

    – Kunst

    3. Oktober 2016 um 14:51 Uhr

  • Das Missverständnis hier ist tatsächlich, dass das Erhöhen von SIGFPE von einer Division durch Null ein definierter Effekt ist und dass daher das Optimieren des Anrufs beobachtbares definiertes Verhalten entfernt.

    – Random832

    3. Oktober 2016 um 15:53 ​​Uhr

  • @ Random832: Ich würde erwarten, dass es Implementierungen gibt, die tun Definieren Sie dies als Effekt, obwohl die Dokumentation schlampig sein kann, was garantiert ist oder nicht (zu sagen, dass ein Signal ausgelöst werden könnte, wäre bedeutungslos, wenn sich die Implementierung auch auf andere Weise willkürlich verhalten könnte). Es wäre wahrscheinlich am logischsten für eine Implementierung zu spezifizieren, dass eine Division durch Null keine Nebeneffekte hat außer die Ausgabe eines Signals und wird kein ergeben beobachtbar Wert ohne Signal, muss aber kein Signal erzeugen, wenn der Quotient nicht in beobachtbarer Weise verwendet wird.

    – Superkatze

    3. Oktober 2016 um 18:48 Uhr


  • Ich akzeptiere diese Antwort, weil ich denken Dies ist eigentlich ein Fehler oder zumindest ein unbeabsichtigtes Verhalten von gcc (Entfernung der Anweisung ohne Warnung). Ich werde aber zuerst ein bisschen mehr recherchieren; Die Leute verstricken sich immer wieder in das Dividieren durch Null, was, soweit ich das beurteilen kann, irrelevant ist, aber der einfachste Weg, um einen Nebeneffekt auf niedriger Ebene zu erzeugen.

    – BurnsBA

    4. Oktober 2016 um 19:17 Uhr


Ich bin kein Experte mit gcc Interna, aber es scheint, dass Ihr Problem nicht darin besteht, toten Code durch einen Optimierungsdurchgang zu entfernen. Es ist sehr wahrscheinlich, dass der Compiler nicht einmal darüber nachdenkt generieren dieser Code an erster Stelle.

Lassen Sie uns Ihr Beispiel von Compiler-spezifischen Intrinsics auf eine einfache alte Ergänzung reduzieren:

int foo(int num) {
    num + 77;
    return num + 15;
}

Kein Code für + 77 generiert:

foo(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        add     eax, 15
        pop     rbp
        ret

Wenn einer der Operanden Nebenwirkungen hat, nur dieser Operand wird ausgewertet. Immer noch keine Ergänzung in der Versammlung.

Aber das Speichern dieses Ergebnisses in eine (sogar unbenutzte) Variable zwingt den Compiler dazu Generieren Sie Code zum Hinzufügen:

int foo(int num) {
  int baz = num + 77;
  return num + 15;
}

Montage:

foo(int):
    push    rbp
    mov     rbp, rsp
    mov     DWORD PTR [rbp-20], edi
    mov     eax, DWORD PTR [rbp-20]
    add     eax, 77
    mov     DWORD PTR [rbp-4], eax
    mov     eax, DWORD PTR [rbp-20]
    add     eax, 15
    pop     rbp
    ret

Das Folgende ist nur eine Spekulation, aber aus meiner Erfahrung mit dem Bau von Compilern ist es natürlicher, den Code für nicht verwendete Ausdrücke nicht zu generieren, anstatt diesen Code später zu entfernen.

Meine Empfehlung ist, Ihre Absichten deutlich zu machen und das Ergebnis in einen Ausdruck zu setzen flüchtig (und daher vom Optimierer nicht entfernbar) Variable.

@Matthieu M wies darauf hin, dass es nicht ausreicht, die Vorberechnung des Werts zu verhindern. Für etwas mehr als das Spielen mit Signalen sollten Sie also dokumentierte Methoden verwenden, um genau die Anweisung auszuführen, die Sie möchten (wahrscheinlich, volatile Inline-Montage).

  • Leider einfach das Ergebnis eingeben volatile reicht nicht aus, um den Compiler daran zu hindern, ihn vorab zu berechnen, ohne tatsächlich die gewünschte Anweisung auszugeben, da alle Parameter zur Kompilierzeit vorhanden sind.

    – Matthias M.

    3. Oktober 2016 um 16:01 Uhr

  • In der Tat; Wenn Sie die Null in einen flüchtigen Wert schieben und erneut auslesen, wird der Optimierer geschlossen.

    – Josua

    3. Oktober 2016 um 16:44 Uhr

  • @MatthieuM. Ich stimme vollkommen zu, dass es nicht immer ausreichend ist, aber in diesem Fall ist es (der letzte Link). Auch Compiler muss Bevorzugen Sie im allgemeinen Fall einen schnelleren Code, um die gewünschte Anweisung auszugeben, um nützlich zu sein. Dies ist der Fall, wenn eine Inline-Montage verwendet werden sollte.

    Benutzer2512323

    3. Oktober 2016 um 16:47 Uhr

  • @deniss: Leider bin ich aus Erfahrung vorsichtig, an “ausreichend” zu glauben. Compiler sind die kniffligen Biester, die sie sind, und neigen dazu, ihr Verhalten bei der geringsten Änderung ihrer Eingaben zu ändern.

    – Matthias M.

    3. Oktober 2016 um 17:26 Uhr

1408810cookie-checkgcc -O0 optimiert weiterhin “unbenutzten” Code. Gibt es ein Compiler-Flag, um das zu ändern?

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

Privacy policy