Wie kann verhindert werden, dass GCC eine ausgelastete Warteschleife optimiert?

Lesezeit: 11 Minuten

Benutzeravatar von Denilson Sá Maia
Denilson Sá Maia

Ich möchte eine C-Code-Firmware für Atmel AVR-Mikrocontroller schreiben. Ich werde es mit GCC kompilieren. Außerdem möchte ich Compiler-Optimierungen aktivieren (-Os oder -O2), da ich keinen Grund sehe, sie nicht zu aktivieren, und sie werden wahrscheinlich viel schneller eine bessere Assembly generieren, als die Assembly manuell zu schreiben.

Aber ich möchte, dass ein kleines Stück Code nicht optimiert wird. Ich möchte die Ausführung einer Funktion um einige Zeit verzögern und wollte daher eine Do-Nothing-Schleife schreiben, nur um etwas Zeit zu verschwenden. Sie müssen nicht genau sein, warten Sie einfach einige Zeit.

/* How to NOT optimize this, while optimizing other code? */
unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i);
}

Da der Speicherzugriff in AVR viel langsamer ist, möchte ich i und j in CPU-Registern gehalten werden.


Update: gerade gefunden util/delay.h und util/delay_basic.h aus AVR Libc. Obwohl es meistens eine bessere Idee ist, diese Funktionen zu verwenden, bleibt diese Frage gültig und interessant.


Verwandte Fragen:

  • Wie kann man verhindern, dass gcc einige Anweisungen in C optimiert?
  • Gibt es eine Möglichkeit, GCC anzuweisen, einen bestimmten Codeabschnitt nicht zu optimieren?
  • Wie man nicht wegoptimiert – Mechanik einer Torheitsfunktion

  • Vielleicht gibt es eine Art “Sleep”-Systemaufruf? Vielleicht können Sie einfach eine Assemblerlogik einbetten?

    – George

    16. August 2011 um 18:57 Uhr

  • Warum nicht so etwas einfügen volatile asm ("rep; nop;") zu Busy-Pause durch Verschwendung von CPU-Zyklen, die nichts tun?

    Benutzer405725

    16. August 2011 um 18:59 Uhr

  • Warum fügen Sie dieses Stück Code nicht in eine Funktion ein und kompilieren es mit ‘-O0’ getrennt vom Rest Ihres ‘-O2’-Codes? Und sie offensichtlich miteinander zu verbinden.

    – str

    16. August 2011 um 19:00 Uhr


  • @George: Habe gerade nachgesehen. avr-libc hat keine Schlaffunktion, die nur einige Zeit wartet. Stattdessen wird es der CPU zugeordnet sleep Anweisung, die einen der Energiesparmodi startet (was effektiv die CPU stoppt). Gute Idee, trotzdem.

    – Denilson Sá Maia

    16. August 2011 um 19:04 Uhr


  • Leute, ihr gebt Lösungen in die Kommentare! Fügen Sie sie als Antworten hinzu! 🙂

    – Denilson Sá Maia

    16. August 2011 um 19:05 Uhr


Benutzeravatar von Denilson Sá Maia
Denilson Sá Maia

Ich habe diese Antwort entwickelt, nachdem ich einem Link aus der Antwort von dmckee gefolgt war, aber sie verfolgt einen anderen Ansatz als seine / ihre Antwort.

Funktionsattribute Dokumentation von GCC erwähnt:

noinline

Dieses Funktionsattribut verhindert, dass eine Funktion für das Inlining berücksichtigt wird. Wenn die Funktion keine Seiteneffekte hat, gibt es andere Optimierungen als Inlining, die dazu führen, dass Funktionsaufrufe wegoptimiert werden, obwohl der Funktionsaufruf live ist. Um zu verhindern, dass solche Aufrufe wegoptimiert werden, put asm ("");

Das brachte mich auf eine interessante Idee … Anstatt a hinzuzufügen nop Anweisung in der inneren Schleife habe ich versucht, einen leeren Assembler-Code hinzuzufügen, wie folgt:

unsigned char i, j;
j = 0;
while(--j) {
    i = 0;
    while(--i)
        asm("");
}

Und es hat funktioniert! Diese Schleife wurde nicht optimiert und nicht extra nop Anleitung eingefügt.

Was mehr ist, wenn Sie verwenden volatilegcc speichert diese Variablen im RAM und fügt eine Reihe von hinzu ldd und std um sie in temporäre Register zu kopieren. Dieser Ansatz verwendet andererseits nicht volatile und erzeugt keinen solchen Overhead.


Aktualisieren: Wenn Sie Code mit kompilieren -ansi oder -stdmüssen Sie ersetzen asm Stichwort mit __asm__wie in der GCC-Dokumentation beschrieben.

Darüber hinaus können Sie auch verwenden __asm__ __volatile__("") wenn dein Assembly-Anweisung muss dort ausgeführt werden, wo wir sie eingefügt haben (dh darf nicht als Optimierung aus einer Schleife verschoben werden)..

  • Wie würden Sie das in Visual Studio machen? __asm{""} wird nicht funktionieren

    – Wooah

    15. Juli 2015 um 23:04 Uhr

  • Ich bin ein bisschen besorgt über das Abrollen von Schleifen, ich würde diese Version mit expliziten Datenabhängigkeiten empfehlen: stackoverflow.com/a/58758133/895245

    – Ciro Santilli OurBigBook.com

    7. November 2019 um 23:53 Uhr

Erklären i und j Variablen als volatile. Dadurch wird verhindert, dass der Compiler Code mit diesen Variablen optimiert.

unsigned volatile char i, j;

  • Obwohl dies funktioniert, hat es den Nebeneffekt, dass diese Variablen in den Speicher gezwungen werden. Daher liest und schreibt GCC sie bei jeder Schleifeniteration, was ziemlich viel Overhead hinzufügt. (Wie auch immer, wenn ich eine extrem feinkörnige Steuerung möchte, sollte ich Assembly direkt schreiben)

    – Denilson Sá Maia

    30. August 2011 um 3:05 Uhr

  • @DenilsonSá Andererseits stellt das Erzwingen eines Speicherzugriffs sicher, dass das Warten immer gleich lange dauert, unabhängig davon, ob der Wert 16-Bit-kodierbar ist oder nicht.

    – Oswin

    15. April 2016 um 11:46 Uhr

  • @Oswin, kannst du das bitte näher erläutern? Was meinen Sie mit “ob der Wert 16-Bit-kodierbar ist oder nicht”. Von welchem ​​”Wert” sprichst du? Und in was codierbar?

    – Denilson Sá Maia

    15. April 2016 um 13:03 Uhr

  • Nur aus Interesse, könnten Sie sie als markieren volatile register um zu vermeiden, dass sie in Erinnerung bleiben?

    – Mark K. Cowan

    10. Juni 2017 um 16:21 Uhr

  • Keine gute Idee, da das Schreiben dieser Variablen in den Speicher jedes Mal eine zusätzliche Verzögerung hinzufügt und daher weniger feinkörnig ist als eine Schleife mit einem Registerschleifenzähler, ganz zu schweigen davon, dass es den Cache / Datenbus belastet, während Sie nur ” nichts”.

    – Carlo Holz

    28. September 2019 um 20:38 Uhr


Ciro Santilli Benutzeravatar von OurBigBook.com
Ciro Santilli OurBigBook.com

Leer __asm__ Anweisungen reichen nicht aus: Verwenden Sie besser Datenabhängigkeiten

So was:

Haupt c

int main(void) {
    unsigned i;
    for (i = 0; i < 10; i++) {
        __asm__ volatile("" : "+g" (i) : :);

    }
}

Kompilieren und disassemblieren:

gcc -O3 -ggdb3 -o main.out main.c
gdb -batch -ex 'disas main' main.out

Ausgabe:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     nopw   0x0(%rax,%rax,1)
   0x0000000000001048 <+8>:     add    $0x1,%eax
   0x000000000000104b <+11>:    cmp    $0x9,%eax
   0x000000000000104e <+14>:    jbe    0x1048 <main+8>
   0x0000000000001050 <+16>:    xor    %eax,%eax
   0x0000000000001052 <+18>:    retq 

Ich glaube, dass dies robust ist, da es eine explizite Datenabhängigkeit auf die Schleifenvariable legt i wie vorgeschlagen unter: Erzwinge die Anweisungsreihenfolge in C++ und erzeugt die gewünschte Schleife:

Dies markiert i als Eingabe und Ausgabe der Inline-Assemblierung. Dann ist die Inline-Assemblierung eine Blackbox für GCC, die nicht wissen kann, wie sie sich ändert ialso denke ich, dass das wirklich nicht wegoptimiert werden kann.

Wenn ich dasselbe mit einem leeren mache __asm__ wie in:

schlecht.c

int main(void) {
    unsigned i;
    for (i = 0; i < 10; i++) {
        __asm__ volatile("");
    }
}

es scheint, die Schleife und die Ausgaben vollständig zu entfernen:

   0x0000000000001040 <+0>:     xor    %eax,%eax
   0x0000000000001042 <+2>:     retq

Beachte das auch __asm__("") und __asm__ volatile("") sollte gleich sein, da es keine Ausgabeoperanden gibt: Der Unterschied zwischen asm, asm volatile und clobbering memory

Was passiert, wird klarer, wenn wir es ersetzen durch:

__asm__ volatile("nop");

was produziert:

   0x0000000000001040 <+0>:     nop
   0x0000000000001041 <+1>:     nop
   0x0000000000001042 <+2>:     nop
   0x0000000000001043 <+3>:     nop
   0x0000000000001044 <+4>:     nop
   0x0000000000001045 <+5>:     nop
   0x0000000000001046 <+6>:     nop
   0x0000000000001047 <+7>:     nop
   0x0000000000001048 <+8>:     nop
   0x0000000000001049 <+9>:     nop
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq

So sehen wir das GCC eben Schleife abgerollt das nop Schleife in diesem Fall, weil die Schleife klein genug war.

Also, wenn Sie sich auf eine leere verlassen __asm__würden Sie sich auf schwer vorhersehbare Kompromisse zwischen GCC-Binärgröße und -geschwindigkeit verlassen, die bei optimaler Anwendung immer die Schleife für ein Leerzeichen entfernen sollten __asm__ volatile(""); die Codegröße Null hat.

noinline Busy-Loop-Funktion

Wenn die Schleifengröße zur Kompilierzeit nicht bekannt ist, ist ein vollständiges Aufrollen nicht möglich, aber GCC könnte sich trotzdem entscheiden, in Blöcken aufzurollen, was Ihre Verzögerungen inkonsistent machen würde.

Zusammen mit Denilsons Antwort könnte eine Busy-Loop-Funktion wie folgt geschrieben werden:

void __attribute__ ((noinline)) busy_loop(unsigned max) {
    for (unsigned i = 0; i < max; i++) {
        __asm__ volatile("" : "+g" (i) : :);
    }
}

int main(void) {
    busy_loop(10);
}

die zerlegt bei:

Dump of assembler code for function busy_loop:
   0x0000000000001140 <+0>:     test   %edi,%edi
   0x0000000000001142 <+2>:     je     0x1157 <busy_loop+23>
   0x0000000000001144 <+4>:     xor    %eax,%eax
   0x0000000000001146 <+6>:     nopw   %cs:0x0(%rax,%rax,1)
   0x0000000000001150 <+16>:    add    $0x1,%eax
   0x0000000000001153 <+19>:    cmp    %eax,%edi
   0x0000000000001155 <+21>:    ja     0x1150 <busy_loop+16>
   0x0000000000001157 <+23>:    retq   
End of assembler dump.
Dump of assembler code for function main:
   0x0000000000001040 <+0>:     mov    $0xa,%edi
   0x0000000000001045 <+5>:     callq  0x1140 <busy_loop>
   0x000000000000104a <+10>:    xor    %eax,%eax
   0x000000000000104c <+12>:    retq   
End of assembler dump.

Hier die volatile Dies wurde benötigt, um die Assembly als möglicherweise mit Nebenwirkungen zu kennzeichnen, da wir in diesem Fall eine Ausgabevariable haben.

Eine Double-Loop-Version könnte sein:

void __attribute__ ((noinline)) busy_loop(unsigned max, unsigned max2) {
    for (unsigned i = 0; i < max2; i++) {
        for (unsigned j = 0; j < max; j++) {
            __asm__ volatile ("" : "+g" (i), "+g" (j) : :);
        }
    }
}

int main(void) {
    busy_loop(10, 10);
}

GitHub-Upstream.

Verwandte Themen:

  • Endlosschleife in C/C++
  • Beste Möglichkeit, Busy Loop zu implementieren?
  • Anweisungsreihenfolge in C++ erzwingen

Getestet in Ubuntu 19.04, GCC 8.3.0.

Ich bin mir nicht sicher, warum noch nicht erwähnt wurde, dass dieser Ansatz völlig fehlgeleitet ist und durch Compiler-Upgrades usw. leicht gebrochen werden kann. Es wäre viel sinnvoller, den Zeitwert zu bestimmen, auf den Sie warten möchten, und den Strom abzufragen Zeit, bis der gewünschte Wert überschritten wird. Auf x86 könnten Sie verwenden rdtsc für diesen zweck wäre aber der portablere weg anzurufen clock_gettime (oder die Variante für Ihr Nicht-POSIX-Betriebssystem), um die Zeit zu erhalten. Aktuelles x86_64-Linux vermeidet sogar den Syscall für clock_gettime und verwenden rdtsc im Inneren. Oder, wenn Sie die Kosten für einen Systemaufruf tragen können, verwenden Sie einfach clock_nanosleep zunächst…

Ich weiß nicht auf Anhieb, ob die avr-Version des Compilers das unterstützt vollständiger Satz von #pragmas (die interessanten im Link stammen alle aus der gcc-Version 4.4), aber da würden Sie normalerweise anfangen.

  • Wissen Sie zufällig, welche GCC-Option die Optimierung dieser Nichtstun-Schleife aktiviert/deaktiviert? Ich habe versucht, mit #pragma GCC optimize 0 (gefolgt von #pragma GCC reset_options nach der Funktion), aber es deaktiviert ALLE Optimierungen (wie erwartet). Es wäre besser gewesen, nur diesen zu deaktivieren.

    – Denilson Sá Maia

    16. August 2011 um 20:04 Uhr

  • Pragma funktioniert nur für nachträglich definierte Funktionen (und funktioniert auf Funktionsebene).

    – Foo Bah

    16. August 2011 um 20:21 Uhr

  • Ich meine… optimize 0 war zu viel, es speicherte diese Variablen nicht einmal in Registern (sie wurden im Speicher gehalten). Also, wenn ich wüsste, welcher gcc -f Option das Entfernen dieser Nichtstun-Schleife deaktiviert, könnte ich nur diese Option für diese Funktion deaktivieren. Das wäre toll!

    – Denilson Sá Maia

    16. August 2011 um 22:13 Uhr

Patwies Benutzeravatar
Patwie

Für mich wurde auf GCC 4.7.0 leeres asm sowieso mit -O3 wegoptimiert (habe es nicht mit -O2 versucht). und die Verwendung eines i++ im Register oder flüchtig führte zu einer großen Leistungseinbuße (in meinem Fall).

Was ich tat, war eine Verknüpfung mit einer anderen leeren Funktion, die der Compiler beim Kompilieren des “Hauptprogramms” nicht sehen konnte.

Grundsätzlich dies:

Erstellt “helper.c” mit dieser deklarierten Funktion (leere Funktion)

void donotoptimize(){}

Dann zusammengestellt gcc helper.c -c -o helper.o
und dann

while (...) { donotoptimize();}

und verlinke es per gcc my_benchmark.cc helper.o.

Dies gab mir die besten Ergebnisse (und meiner Meinung nach überhaupt keinen Overhead, kann es aber nicht testen, da mein Programm ohne es nicht funktioniert :))

Ich denke, es sollte auch mit icc funktionieren. Vielleicht nicht, wenn Sie Verknüpfungsoptimierungen aktivieren, aber mit gcc tut es das.

  • Wissen Sie zufällig, welche GCC-Option die Optimierung dieser Nichtstun-Schleife aktiviert/deaktiviert? Ich habe versucht, mit #pragma GCC optimize 0 (gefolgt von #pragma GCC reset_options nach der Funktion), aber es deaktiviert ALLE Optimierungen (wie erwartet). Es wäre besser gewesen, nur diesen zu deaktivieren.

    – Denilson Sá Maia

    16. August 2011 um 20:04 Uhr

  • Pragma funktioniert nur für nachträglich definierte Funktionen (und funktioniert auf Funktionsebene).

    – Foo Bah

    16. August 2011 um 20:21 Uhr

  • Ich meine… optimize 0 war zu viel, es speicherte diese Variablen nicht einmal in Registern (sie wurden im Speicher gehalten). Also, wenn ich wüsste, welcher gcc -f Option das Entfernen dieser Nichtstun-Schleife deaktiviert, könnte ich nur diese Option für diese Funktion deaktivieren. Das wäre toll!

    – Denilson Sá Maia

    16. August 2011 um 22:13 Uhr

Benutzeravatar von Groovy
Groovig

Das Setzen von volatile asm sollte helfen. Hier können Sie mehr darüber lesen: –

http://www.nongnu.org/avr-libc/user-manual/optimization.html

Wenn Sie unter Windows arbeiten, können Sie sogar versuchen, den Code unter Pragmas zu platzieren, wie unten im Detail erklärt:-

https://www.securecoding.cert.org/confluence/display/cplusplus/MSC06-CPP.+Achten+auf+Compiler+Optimierung+beim+Umgang+mit+sensiblen+Daten

Hoffe das hilft.

  • Wie in einem Kommentar zu dieser Antwort erwähnt, mit volatile hat den Nebeneffekt, dass diese Variablen mit LD- und ST-Befehlen in den Speicher gezwungen werden.

    – einpoklum

    14. Januar 2016 um 20:20 Uhr

1417600cookie-checkWie kann verhindert werden, dass GCC eine ausgelastete Warteschleife optimiert?

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

Privacy policy