TL: DR: Es ist eine Fehloptimierung durch gcc .
noreturn
ist ein Versprechen an den Compiler, dass die Funktion nicht zurückkehren wird. Dies ermöglicht Optimierungen und ist insbesondere in Fällen nützlich, in denen es für den Compiler schwierig ist, zu beweisen, dass eine Schleife niemals beendet wird, oder auf andere Weise zu beweisen, dass es keinen Pfad durch eine zurückkehrende Funktion gibt.
GCC optimiert bereits, um main
das Ende der Funktion zu func()
verlieren, wenn zurückgegeben wird, selbst mit der Standardeinstellung -O0
(Mindestoptimierungsstufe), die Sie verwendet haben.
Die Ausgabe für func()
sich könnte als verpasste Optimierung angesehen werden. es könnte einfach alles nach dem Funktionsaufruf weglassen (da der Aufruf nicht zurückgegeben werden kann, ist die einzige Möglichkeit, wie die Funktion selbst sein kann noreturn
). Es ist kein gutes Beispiel, da printf
eine Standard-C-Funktion bekanntermaßen normal zurückkehrt (es sei denn, Sie setvbuf
geben stdout
einen Puffer an, der fehlerhaft ist?).
Verwenden wir eine andere Funktion, die der Compiler nicht kennt.
void ext(void);
int foo;
_Noreturn void func(int *p, int a) {
ext();
*p = a;
foo = 1;
}
void bar() {
func(&foo, 3);
}
( Code + x86-64 asm im Godbolt-Compiler-Explorer . )
Die Ausgabe von gcc7.2 für bar()
ist interessant. Es inline func()
und beseitigt den foo=3
toten Speicher, so dass nur:
bar:
sub rsp, 8 ## align the stack
call ext
mov DWORD PTR foo[rip], 1
## fall off the end
Gcc geht weiterhin davon aus, dass ext()
nach Rückkehr wird, sonst könnte es nur Schwanz genannt hat ext()
mit jmp ext
. Aber gcc ruft keine Tailcall- noreturn
Funktionen auf, da dadurch Backtrace-Informationen für Dinge wie verloren gehen abort()
. Anscheinend ist es in Ordnung, sie zu inlinieren.
Gcc hätte optimieren können, indem der mov
Laden auch nach dem weggelassen wurde call
. Wenn ext
zurückgegeben wird, wird das Programm abgespritzt, sodass es keinen Sinn macht, diesen Code zu generieren. Clang macht diese Optimierung in bar()
/ main()
.
func
selbst ist interessanter und eine größere verpasste Optimierung .
gcc und clang strahlen fast dasselbe aus:
func:
push rbp # save some call-preserved regs
push rbx
mov ebp, esi # save function args for after ext()
mov rbx, rdi
sub rsp, 8 # align the stack before a call
call ext
mov DWORD PTR [rbx], ebp # *p = a;
mov DWORD PTR foo[rip], 1 # foo = 1
add rsp, 8
pop rbx # restore call-preserved regs
pop rbp
ret
Diese Funktion kann davon ausgehen, dass sie nicht zurückkehrt und verwendet rbx
und rbp
ohne sie zu speichern / wiederherzustellen.
Gcc für ARM32 macht das tatsächlich, gibt aber immer noch Anweisungen aus, um ansonsten sauber zurückzukehren. Eine noreturn
Funktion, die tatsächlich auf ARM32 zurückkehrt, unterbricht den ABI und verursacht schwer zu debuggende Probleme im Aufrufer oder später. (Undefiniertes Verhalten erlaubt dies, aber es handelt sich zumindest um ein Problem mit der Qualität der Implementierung: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82158 .)
Dies ist eine nützliche Optimierung in Fällen, in denen gcc nicht beweisen kann, ob eine Funktion zurückkehrt oder nicht. (Es ist jedoch offensichtlich schädlich, wenn die Funktion einfach zurückkehrt. Gcc warnt, wenn sicher ist, dass eine Noreturn-Funktion zurückkehrt.) Andere gcc-Zielarchitekturen tun dies nicht. Das ist auch eine verpasste Optimierung.
Aber gcc geht nicht weit genug: Wenn Sie auch die Rückgabeanweisung wegoptimieren (oder durch eine unzulässige Anweisung ersetzen), sparen Sie Code und garantieren einen lauten Fehler anstelle einer stillen Beschädigung.
Und wenn Sie das ret
optimieren möchten, ist es sinnvoll, alles zu optimieren , was nur benötigt wird, wenn die Funktion zurückkehrt.
So func()
könnte zusammengestellt werden zu :
sub rsp, 8
call ext
# *p = a; and so on assumed to never happen
ud2 # optional: illegal insn instead of fall-through
Jede andere vorhandene Anweisung ist eine verpasste Optimierung. Wenn ext
deklariert noreturn
wird, bekommen wir genau das.
Es kann davon ausgegangen werden, dass jeder Basisblock , der mit einer Rückgabe endet, niemals erreicht wird.