GCC 4.4:Bereichsprüfung bei switch/case-Anweisung in gcc vermeiden?
-
15-09-2020 - |
Frage
Dies ist nur ein Problem bei GCC-Versionen vor 4.4, es wurde in GCC 4.5 behoben.
Ist es möglich, dem Compiler mitzuteilen, dass die in einem Schalter verwendete Variable in die bereitgestellten Case-Anweisungen passt?Insbesondere, wenn es sich um einen kleinen Bereich handelt und eine Sprungtabelle generiert wird.
extern int a;
main()
{
switch (a & 0x7) { // 0x7 == 111 values are 0-7
case 0: f0(); break;
case 1: f1(); break;
case 2: f2(); break;
case 3: f3(); break;
case 4: f4(); break;
case 5: f5(); break;
case 6: f6(); break;
case 7: f7(); break;
}
}
Ich habe versucht, auf niedrige Bits zu xorieren (wie im Beispiel), Enumerationen zu verwenden und gcc_unreachable() zu verwenden, ohne Erfolg.Der generierte Code prüft immer, ob die Variable innerhalb des Bereichs liegt, fügt eine sinnlose Verzweigungsbedingung hinzu und entfernt den Sprungtabellen-Berechnungscode.
Notiz:Da dies in der innersten Schleife eines Decoders geschieht, ist die Leistung von entscheidender Bedeutung.
Es scheint, dass ich nicht der bin nur eins.
Es gibt keine Möglichkeit, GCC zu sagen, dass der Standardzweig niemals eingenommen wird, obwohl er den Standardzweig weglassen kann, wenn er nachweisen kann, dass der Wert niemals außerhalb der Bereiche basierend auf früheren bedingten Schecks ist.
Wie können Sie also gcc dabei helfen, zu beweisen, dass die Variable passt und es im obigen Beispiel keinen Standardzweig gibt?(Natürlich ohne das Hinzufügen eines bedingten Zweigs.)
Aktualisierung
Dies geschah unter OS
Die Funktionen im Beispiel dienen der besseren Lesbarkeit. Denken Sie, dass es sich dabei um Inline-Funktionen oder nur um Anweisungen handelt.Ein Funktionsaufruf auf x86 durchzuführen ist teuer.
Auch das Beispiel gehört, wie im Hinweis erwähnt, in eine Datenschleife (Big Data).
Der mit gcc 4.2/OS X generierte Code lautet:
[...] andl $7, %eax cmpl $7, %eax ja L11 mov %eax, %eax leaq L20(%rip), %rdx movslq (%rdx,%rax,4),%rax addq %rdx, %rax jmp *%rax .align 2,0x90 L20: .long L12-L20 .long L13-L20 .long L14-L20 .long L15-L20 .long L16-L20 .long L17-L20 .long L18-L20 .long L19-L20 L19: [...]
Das Problem liegt darin
cmp $7, %eax;
ja L11;
OK, ich entscheide mich für die hässliche Lösung und füge einen Sonderfall für gcc-Versionen unter 4.4 hinzu, indem ich eine andere Version ohne Schalter verwende und die &&label-Erweiterungen von goto und gcc verwende.
static void *jtb[] = { &&c_1, &&c_2, &&c_3, &&c_4, &&c_5, &&c_6, &&c_7, &&c_8 }; [...] goto *jtb[a & 0x7]; [...] while(0) { c_1: // something break; c_2: // something break; [...] }
Beachten Sie, dass das Label-Array statisch ist und daher nicht bei jedem Aufruf berechnet wird.
Lösung
Ich habe versucht, etwas Einfaches und Vergleichbares mit -O5 und -fno-inline zu kompilieren (meine f0-f7-Funktionen waren trivial) und es hat Folgendes generiert:
8048420: 55 push %ebp ;; function preamble
8048421: 89 e5 mov %esp,%ebp ;; Yeah, yeah, it's a function.
8048423: 83 ec 04 sub $0x4,%esp ;; do stuff with the stack
8048426: 8b 45 08 mov 0x8(%ebp),%eax ;; x86 sucks, we get it
8048429: 83 e0 07 and $0x7,%eax ;; Do the (a & 0x7)
804842c: ff 24 85 a0 85 04 08 jmp *0x80485a0(,%eax,4) ;; Jump table!
8048433: 90 nop
8048434: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
8048438: 8d 45 08 lea 0x8(%ebp),%eax
804843b: 89 04 24 mov %eax,(%esp)
804843e: e8 bd ff ff ff call 8048400
8048443: 8b 45 08 mov 0x8(%ebp),%eax
8048446: c9 leave
Haben Sie versucht, mit Optimierungsstufen zu spielen?
Andere Tipps
Vielleicht könnten Sie anstelle eines Schalters ein Array von Funktionszeigern verwenden?
#include <stdio.h>
typedef void (*func)(void);
static void f0(void) { printf("%s\n", __FUNCTION__); }
static void f1(void) { printf("%s\n", __FUNCTION__); }
static void f2(void) { printf("%s\n", __FUNCTION__); }
static void f3(void) { printf("%s\n", __FUNCTION__); }
static void f4(void) { printf("%s\n", __FUNCTION__); }
static void f5(void) { printf("%s\n", __FUNCTION__); }
static void f6(void) { printf("%s\n", __FUNCTION__); }
static void f7(void) { printf("%s\n", __FUNCTION__); }
int main(void)
{
const func f[8] = { f0, f1, f2, f3, f4, f5, f6, f7 };
int i;
for (i = 0; i < 8; ++i)
{
f[i]();
}
return 0;
}
Haben Sie versucht, das zu deklarieren? switch
Variable als Bitfeld?
struct Container {
uint16_t a:3;
uint16_t unused:13;
};
struct Container cont;
cont.a = 5; /* assign some value */
switch( cont.a ) {
...
}
Hoffe, das funktioniert!
Ich habe es nicht versucht, bin mir aber nicht sicher gcc_unreachable
macht das Gleiche wie __builtin_unreachable
.Googelt die beiden, gcc_unreachable
scheint als Assertionstool für die Entwicklung von GCC selbst konzipiert zu sein, möglicherweise mit einem Hinweis zur Verzweigungsvorhersage __builtin_unreachable
macht das Programm sofort undefiniert – was sich anhört, als würde man den Grundblock löschen, was Sie wollen.
Vielleicht verwenden Sie einfach a default
Bezeichnung für den ersten oder letzten Fall?
Diese Frage ist unter dem Gesichtspunkt einer für uns scheinbar offensichtlichen fehlenden Compiler-Optimierung sicherlich interessant, und ich habe viel Zeit damit verbracht, eine einfache Lösung zu finden, größtenteils aus persönlicher Neugier.
Trotzdem muss ich zugeben Ich bin sehr skeptisch, dass diese zusätzliche Anweisung jemals zu einem messbaren Leistungsunterschied führen wird in der Praxis, insbesondere auf einem neuen Mac.Wenn Sie über eine beträchtliche Datenmenge verfügen, sind Sie an E/A gebunden und eine einzelne Anweisung wird niemals Ihr Engpass sein.Wenn Sie über eine kleine Datenmenge verfügen, müssen Sie eine durchführen viel, viel, viel Es müssen mehrere Berechnungen wiederholt werden, bevor eine einzelne Anweisung zum Engpass wird.
Würden Sie Code veröffentlichen, um zu zeigen, dass es tatsächlich einen Leistungsunterschied gibt?Oder beschreiben Sie den Code und die Daten, mit denen Sie arbeiten?