Zusammenfassung:
Ich suche den schnellsten Weg, um zu berechnen
(int) x / (int) y
ohne eine Ausnahme zu bekommen für y==0
. Stattdessen möchte ich nur ein beliebiges Ergebnis.
Hintergrund:
Beim Codieren von Bildverarbeitungsalgorithmen muss ich oft durch einen (akkumulierten) Alpha-Wert dividieren. Die einfachste Variante ist einfacher C-Code mit ganzzahliger Arithmetik. Mein Problem ist, dass ich für Ergebnispixel mit normalerweise einen Fehler durch Division durch Null erhalte alpha==0
. Dies sind jedoch genau die Pixel, bei denen das Ergebnis überhaupt keine Rolle spielt: Ich interessiere mich nicht für Farbwerte von Pixeln mit alpha==0
.
Einzelheiten:
Ich suche so etwas wie:
result = (y==0)? 0 : x/y;
oder
result = x / MAX( y, 1 );
x und y sind positive ganze Zahlen. Der Code wird sehr oft in einer verschachtelten Schleife ausgeführt, daher suche ich nach einer Möglichkeit, die bedingte Verzweigung zu beseitigen.
Wenn y den Bytebereich nicht überschreitet, bin ich mit der Lösung zufrieden
unsigned char kill_zero_table[256] = { 1, 1, 2, 3, 4, 5, 6, 7, [...] 255 };
[...]
result = x / kill_zero_table[y];
Dies funktioniert jedoch offensichtlich nicht gut für größere Reichweiten.
Ich denke, die letzte Frage ist: Was ist der schnellste Bit-Twiddling-Hack, der 0 in einen anderen ganzzahligen Wert ändert, während alle anderen Werte unverändert bleiben?
Klarstellungen
Ich bin mir nicht 100% sicher, dass die Verzweigung zu teuer ist. Es werden jedoch unterschiedliche Compiler verwendet, daher bevorzuge ich Benchmarking mit geringen Optimierungen (was in der Tat fraglich ist).
Natürlich sind Compiler großartig, wenn es um Bit-Twiddling geht, aber ich kann das Ergebnis "egal" in C nicht ausdrücken, sodass der Compiler niemals alle Optimierungen nutzen kann.
Der Code sollte vollständig C-kompatibel sein. Die Hauptplattformen sind Linux 64 Bit mit gcc & clang und MacOS.
quelle
y += !y
? Kein Zweig benötigt, um das zu berechnen. Sie könnten vergleichenx / (y + !y)
gegenx / max(y, 1)
und vielleicht auchy ? (x/y) : 0
. Ich denke, es wird in keinem von beiden einen Zweig geben, zumindest wenn die Optimierungen aktiviert sind.0
Abschnitte groß und zusammenhängend sind. Es gibt einen Ort, an dem man mit Mikrooptimierungen herumspielen kann, und Operationen pro Pixel sind genau dieser Ort.Antworten:
Inspiriert von einigen Kommentaren habe ich den Zweig auf meinem Pentium und
gcc
Compiler mit entferntDer Compiler erkennt grundsätzlich, dass er im Zusatz ein Bedingungsflag des Tests verwenden kann.
Auf Anfrage der Baugruppe:
Da sich herausstellte, dass dies eine so beliebte Frage und Antwort war, werde ich etwas näher darauf eingehen. Das obige Beispiel basiert auf einer Programmiersprache, die ein Compiler erkennt. Im obigen Fall wird ein boolescher Ausdruck in der Integralarithmetik verwendet, und die Verwendung von Bedingungsflags wird zu diesem Zweck in Hardware erfunden. Im Allgemeinen sind Flags in C nur mit idiom zugänglich. Aus diesem Grund ist es in C so schwierig, eine tragbare Ganzzahlbibliothek mit mehrfacher Genauigkeit zu erstellen, ohne auf (Inline-) Assembly zurückgreifen zu müssen. Ich vermute, dass die meisten anständigen Compiler die obige Redewendung verstehen werden.
Eine andere Möglichkeit, Verzweigungen zu vermeiden, wie auch in einigen der obigen Kommentare erwähnt, ist die prädizierte Ausführung. Ich habe daher den ersten Code von philipp und meinen Code genommen und ihn durch den Compiler von ARM und den GCC-Compiler für die ARM-Architektur ausgeführt, die eine prädizierte Ausführung bietet. Beide Compiler vermeiden die Verzweigung in beiden Codebeispielen:
Philipps Version mit dem ARM-Compiler:
Philipps Version mit GCC:
Mein Code mit dem ARM-Compiler:
Mein Code mit GCC:
Alle Versionen benötigen noch eine Verzweigung zur Divisionsroutine, da diese Version des ARM keine Hardware für eine Division enthält, der Test für
y == 0
jedoch vollständig durch prädizierte Ausführung implementiert wird.quelle
constexpr
und unnötigetemplate<typename T, typename U> constexpr auto fdiv( T t, U u ) -> decltype(t/(u+!u)) { return t/(u+!u); }
255
(lhs)/(rhs+!rhs) & -!rhs
|
nicht&
. Hoppla -( (lhs)/(rhs+!rhs) ) | -!rhs
sollte Ihren Wert auf0xFFFFFFF
ifrhs
is0
undlhs/rhs
if setzenrhs!=0
.Hier sind einige konkrete Zahlen unter Windows mit GCC 4.7.2:
Beachten Sie, dass ich absichtlich nicht anrufe
srand()
, sodassrand()
immer genau die gleichen Ergebnisse zurückgegeben werden. Beachten Sie auch, dass-DCHECK=0
nur die Nullen gezählt werden, so dass es offensichtlich ist, wie oft erschienen.Nun können Sie es auf verschiedene Arten kompilieren und zeitlich festlegen:
zeigt die Ausgabe, die in einer Tabelle zusammengefasst werden kann:
Wenn Nullen selten sind, funktioniert die
-DCHECK=2
Version schlecht. Wenn mehr Nullen erscheinen, wird die-DCHECK=2
Fall deutlich besser. Von den anderen Optionen gibt es wirklich keinen großen Unterschied.Denn
-O3
es ist eine andere Geschichte:Dort hat Scheck 2 im Vergleich zu den anderen Schecks keinen Nachteil und behält die Vorteile bei, wenn Nullen häufiger werden.
Sie sollten jedoch wirklich messen, um zu sehen, was mit Ihrem Compiler und Ihren repräsentativen Beispieldaten passiert.
quelle
d=0
, dass 50% der Einträge zufällig sind, anstatt fast immerd!=0
, und Sie werden mehr Fehler bei der Verzweigungsvorhersage sehen. Die Vorhersage von Zweigen ist großartig, wenn einem Zweig fast immer gefolgt wird oder wenn das Folgen des einen oder anderen Zweigs wirklich klumpig ist ...d
Iteration ist die innere Schleife, daher sind died == 0
Fälle gleichmäßig verteilt. Und sind 50% der Fälled == 0
realistisch?0.002%
die Fälled==0
realistisch? Sie werden über alle 65000 Iterationen verteilt, die Sie in Ihremd==0
Fall getroffen haben. Während50%
möglicherweise nicht oft10%
oder1%
leicht passieren könnte, oder sogar90%
oder99%
. Der angezeigte Test testet nur wirklich "Wenn Sie im Grunde nie einen Zweig hinuntergehen, macht die Verzweigungsvorhersage das Entfernen des Zweigs sinnlos?", Worauf die Antwort "Ja, aber das ist nicht interessant" lautet.Ohne Kenntnis der Plattform gibt es keine Möglichkeit, die genau effizienteste Methode zu ermitteln. Auf einem generischen System kann dies jedoch nahe am Optimum liegen (unter Verwendung der Intel-Assembler-Syntax):
(Angenommen, der Divisor ist in
ecx
und die Dividende ist ineax
)Vier unverzweigte Einzelzyklusanweisungen plus die Teilung. Der Quotient ist in
eax
und der Rest istedx
am Ende. (Diese Art zeigt, warum Sie keinen Compiler senden möchten, um die Arbeit eines Mannes zu erledigen).quelle
Laut diesem Link können Sie das SIGFPE-Signal einfach mit blockieren
sigaction()
(ich habe es nicht selbst ausprobiert, aber ich glaube, es sollte funktionieren).Dies ist der schnellstmögliche Ansatz, wenn Fehler durch Division durch Null äußerst selten sind: Sie zahlen nur für die Division durch Null, nicht für die gültigen Divisionen, der normale Ausführungspfad wird überhaupt nicht geändert.
Das Betriebssystem ist jedoch an jeder Ausnahme beteiligt, die ignoriert wird, was teuer ist. Ich denke, Sie sollten mindestens tausend gute Divisionen pro Division durch Null haben, die Sie ignorieren. Wenn Ausnahmen häufiger auftreten, zahlen Sie wahrscheinlich mehr, wenn Sie die Ausnahmen ignorieren, als indem Sie jeden Wert vor der Division überprüfen.
quelle