Ist es besser, den Mod-Operator möglichst zu vermeiden?
Lesezeit: 13 Minuten
limp_chimp
Ich nehme an, dass die Berechnung des Moduls einer Zahl eine ziemlich teure Operation ist, zumindest im Vergleich zu einfachen arithmetischen Tests (wie zum Beispiel zu sehen, ob eine Zahl die Länge eines Arrays überschreitet). Wenn dies tatsächlich der Fall ist, ist es effizienter, beispielsweise den folgenden Code zu ersetzen:
res = array[(i + 1) % len];
mit den folgenden? :
res = array[(i + 1 == len) ? 0 : i + 1];
Der erste ist angenehmer für die Augen, aber ich frage mich, ob der zweite effizienter sein könnte. Wenn ja, kann ich erwarten, dass ein optimierender Compiler das erste Snippet durch das zweite ersetzt, wenn eine kompilierte Sprache verwendet wird?
Natürlich funktioniert diese “Optimierung” (sofern es sich tatsächlich um eine Optimierung handelt) nicht in allen Fällen (in diesem Fall funktioniert sie nur, wenn i+1 ist nie mehr als len).
Da fehlt vielleicht der Wald vor lauter Bäumen.
– Burhan Khalid
24. März 2013 um 7:58 Uhr
wenn len ist eine Kompilierzeitkonstante eines aktuellen GCC-Compilers (mit -02) macht normalerweise clevere Dinge und vermeidet oft die Modulus-Maschinenanweisung des Zielprozessors.
– Basile Starynkevitch
24. März 2013 um 7:59 Uhr
Das ist wirklich die Art von Optimierung, die Sie vergessen sollten. Der optimierende Compiler wird es besser machen, als Sie es könnten. Viel wichtiger ist die Lesbarkeit Ihres Codes.
– Basile Starynkevitch
24. März 2013 um 8:04 Uhr
Oder Sie könnten das Array 1 länger machen und das erste Element in das neue letzte Element kopieren, damit Sie normal darauf zugreifen können. Je nach Umständen kann jede dieser drei Optionen die schnellste sein.
– Harald
24. März 2013 um 12:08 Uhr
Dies wird normalerweise in kreisförmigen Warteschlangen verwendet
– Mohamed El-Nakeep
13. Dezember 2013 um 19:29 Uhr
NPE
Mein allgemeiner Rat lautet wie folgt. Verwenden Sie die Version, die Ihrer Meinung nach für das Auge angenehmer ist, und profilieren Sie dann Ihr gesamtes System. Optimieren Sie nur die Teile des Codes, die der Profiler als Engpässe kennzeichnet. Ich wette meinen letzten Dollar, dass der Modulo-Operator nicht darunter sein wird.
Was das spezifische Beispiel angeht, kann nur Benchmarking sagen, was auf Ihrer spezifischen Architektur mit Ihrem spezifischen Compiler schneller ist. Sie ersetzen möglicherweise Modulo durch Verzweigungund es ist alles andere als offensichtlich, was schneller wäre.
Auf neueren Maschinen ist die Integer-Arithmetik fast kostenlos; Was viel mehr zählt, sind Cache-Fehlschläge … die viel kostspieliger sind. ein L1-Cache-Fehlschlag hält den Prozessor für Hunderte von Zyklen an, während derer der Prozessor Dutzende von Divisionen oder Modulus ausführen könnte; die letztendlichen Kosten des Moduls sind also Rauschen
– Basile Starynkevitch
24. März 2013 um 8:00 Uhr
@BasileStarynkevitch: Nun, das Cache-Verhalten wird zwischen den beiden Code-Snippets identisch sein. Entscheidend ist, ob Version 2 Verzweigungen verwendet oder nicht, und wenn ja, wie gut der Verzweigungsprädiktor sein wird.
– NPE
24. März 2013 um 8:02 Uhr
@Basile Starynkevitch Ich habe einen Faktor von etwa 300 zwischen Modulo und dem Zugriff auf eine große Tabelle auf einem Laptop gesehen. (Das Hinzufügen eines Tests auf Teilbarkeit durch 17 zum Quadrat, um den Array-Zugriff zu vermeiden, war immer noch von Vorteil.)
– sternenblau
24. März 2013 um 9:44 Uhr
@NPE Es könnte sich lohnen, dieser Antwort hinzuzufügen, dass die C-Sprache selbst keine Geschwindigkeit hat. Das ist eine Qualität der Implementierung (z. B. “Ihre spezifische Architektur”). “Die Geschwindigkeit des Modulo-Operators” hängt nicht nur von der Hardware ab, sondern auch von der Qualität des Compiler-Baucodes für die Hardware; Ein schlechter könnte das Assembly-Äquivalent von verwenden int foo = 54321; int bar = foo / 10000; foo -= bar * 10000; um das Modulo zu erhalten, während ein guter Compiler diesen Code sogar optimieren könnte.
– autistisch
24. März 2013 um 10:49 Uhr
Einige einfache Messungen:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int test = atoi(argv[1]);
int divisor = atoi(argv[2]);
int iterations = atoi(argv[3]);
int a = 0;
if (test == 0) {
for (int i = 0; i < iterations; i++)
a = (a + 1) % divisor;
} else if (test == 1) {
for (int i = 0; i < iterations; i++)
a = a + 1 == divisor ? 0 : a + 1;
}
printf("%d\n", a);
}
Kompilieren entweder mit gcc oder clang with -O3und läuft time ./a.out 0 42 1000000000 (Modulo-Version) bzw time ./a.out 1 42 1000000000 (Vergleichsversion) ergibt
6,25 Sekunden Benutzerlaufzeit für die Modulo-Version,
1,03 Sekunden für die Vergleichsversion.
(unter Verwendung von gcc 5.2.1 oder clang 3.6.2; Intel Core i5-4690K @ 3,50 GHz; 64-Bit-Linux)
Das bedeutet, dass es wahrscheinlich eine gute Idee ist, die Vergleichsversion zu verwenden.
Bei realistischeren Daten (z. B. wenn die Zahl zufällig wäre) wäre der Unterschied nicht so groß
– Benutzer1209304
24. Oktober 2016 um 12:23 Uhr
Die Vergleichsversion ist nur schneller, weil das Ergebnis der if-Anweisung jedes Mal dasselbe ist, sodass der Verzweigungsprädiktor es jedes Mal richtig macht. Wenn Sie die Eingabe randomisiert haben, ist die Vergleichsversion möglicherweise sogar schlechter als mod
– Bigminimus
26. Juni 2017 um 10:39 Uhr
@Bigminimus Hmm, aber das Ergebnis der if-Klausel ist für beide Tests immer gleich?
– Baris Demiray
9. August 2017 um 12:54 Uhr
Er verweist auf den (?) Operator, Ihr Code, abhängig von der Größe des Divisors, errät nur 1 von 100, 400 usw. falsch
– Garret Gang
10. September 2019 um 21:31 Uhr
Michel Billaud
Schauen Sie sich 2 Möglichkeiten an, um den nächsten Wert eines zyklischen “Modulo 3” -Zählers zu erhalten.
int next1(int n) {
return (n + 1) % 3;
}
int next2(int n) {
return n == 2 ? 0 : n + 1;
}
Ich habe es mit der Option gcc -O3 (für die gängige x64-Architektur) und -s kompiliert, um den Assemblercode zu erhalten.
Der Code für die erste Funktion bewirkt unerklärliche Magie
Interessanterweise führt das gleiche Experiment mit 4 statt 3 zu einer Und-Maskierung für die erste Funktion
aber es ist immer noch und im Großen und Ganzen der zweiten Version unterlegen.
int next3(int n) {
return (n + 1) & 3;;
}
Deutlicher über die richtige Art und Weise, die Dinge zu tun, sprechen
leal 1(%rdi), %eax
andl $3, %eax
ret
liefert deutlich bessere Ergebnisse:
naja, nicht so kompliziert. Multiplikation mit Kehrwert. Berechnen Sie die ganzzahlige Konstante K = (2^N)/3 für einen ausreichend großen Wert von N. Wenn Sie nun den Wert von X/3 anstelle einer Division durch 3 wollen, berechnen Sie X*K und verschieben Sie ihn N Positionen nach rechts.
Der Vergleich in der zweiten Version könnte sie der ersten Version unterlegen machen; Wenn es nicht regelmäßig den richtigen Zweig vorhersagt, wird die Pipeline durcheinander gebracht. Trotzdem +1, um zu demonstrieren, dass moderne Compiler nicht immer auf magische Weise den bestmöglichen Maschinencode finden.
@Ray Soweit ich weiß, wurde dem Befehlssatz (Pentium Pro) eine bedingte Bewegung hinzugefügt, sodass überhaupt keine Verzweigungsvorhersage stattfindet und die Möglichkeit falscher Verzweigungsvorhersagen durch den Prozessor.” Pentium-Pro Family Developers Manual, Band 2, S. 6.14.
phatcode.net/res/231/files/24269101.pdf
– Michel Billaud
25. September 2018 um 10:08 Uhr
Michel Billaud: Sieht so aus, als hätten Sie Recht. Ich habe das cmpl gesehen und das Fehlen eines Sprungs völlig übersehen.
– Strahl % 4 25. September 2018 um 15:20 Uhr Das Code ist komplexer, weil Sie tun unsigned intunterzeichnet & 3 Arithmetik. Laut C99 muss das Vorzeichen des Moduls mit dem Vorzeichen des Dividenden übereinstimmen, es handelt sich also nicht nur um ein einfaches bitweises UND. Ändern Sie den Typ in
und Sie erhalten das gleiche Ergebnis wie die
Code.
– pat
22. Februar 2020 um 3:00 Uhr
-1, weil die Antwort stark darauf hindeutet, nach Codegröße zu urteilen, was eine gute Heuristik ist, aber ein Fehler, wenn es um Optimierungen wie die in dieser Frage geht. Nicht alle Anweisungen sind gleich. Selbst auf einer RISC-Architektur können einige Operationen länger dauern als andere, und auf einer Pipeline-CPU die Zeit, die für die Ausführung einer falsch vorhergesagten Verzweigung aufgewendet wird (die länger ist als die Verzweigung selbst, aber nach der Verzweigung fortgesetzt wird, bis das Ergebnis der Verzweigungsbedingung tiefer gefunden wird). der Pipeline) kann länger sein als die Zeit, die der unbedingte Code mit mehr Anweisungen verbringt.
– mtraceur
#include <iostream>
#include <array>
#include <algorithm>
#include <random>
#include <chrono>
using namespace std::chrono;
constexpr size_t iter = 1e8;
int main() {
std::minstd_rand rnd_engine{1234};
std::uniform_int_distribution<int> dist {-1000, 1000};
auto gen = [&]() { return dist(rnd_engine); };
std::array<int, 10> a;
std::generate( a.begin(), a.end(), gen);
for (size_t size = 2; size < 10; size++) {
std::cout << "Modulus size = " << size << '\n';
{
std::cout << "operator% ";
long sum = 0;
size_t x = 0;
auto start = high_resolution_clock::now();
for (size_t i = 0; i < iter; ++i) {
sum += a[x];
x = (x + 1) % size;
}
auto stop = high_resolution_clock::now();
std::cout << duration_cast<microseconds>(stop - start).count()*0.001
<< "ms\t(sum = " << sum << ")\n";
}
{
std::cout << "ternary ";
long sum = 0;
size_t x = 0;
auto start = high_resolution_clock::now();
for (size_t i = 0; i < iter; ++i) {
sum += a[x];
x = ((x + 1) == size) ? 0 : x + 1;
}
auto stop = high_resolution_clock::now();
std::cout << duration_cast<microseconds>(stop - start).count()*0.001
<< "ms\t(sum = " << sum << ")\n";
}
{
std::cout << "branchless ";
long sum = 0;
size_t x = 1;
auto start = high_resolution_clock::now();
for (size_t i = 0; i < iter; ++i) {
sum += a[x-1];
x = ( x != size ) * x + 1;
}
auto stop = high_resolution_clock::now();
std::cout << duration_cast<microseconds>(stop - start).count()*0.001
<< "ms\t(sum = " << sum << ")\n";
}
}
return 0;
}
Hier ist ein zusätzlicher Benchmark. Beachten Sie, dass ich auch eine zweiglose Version hinzugefügt habe: operator% Und hier ist die Ausgabe auf meinem i7-4870HQ
In diesem speziellen Fall sieht der ternäre Operator weit überlegen aus, und es wird noch mehr so, wenn der Verzweigungsprädiktor hochfährt. Beachten Sie jedoch, dass dies ein sehr spezieller Fall ist: Wenn wir den Index nicht um einen nicht konstanten Wert erhöhen würden, verwenden wir den allgemeineren
wäre einfach, während die anderen beiden Methoden sehr kompliziert werden könnten. Ich möchte diesen sehr unterschätzten Kommentar hervorheben: Wenn len eine Kompilierzeitkonstante ist, ist dies ein neuerer GCC-Compiler (mit -02). normalerweise schlaue Dinge tun, oft die Modulus-Maschine vermeiden
Anweisung des Zielprozessors. size – Basile Starynkevitch const size_t size = 4; Zum Beispiel durch Entfernen der Schlaufe an der
Die Ausführungszeit der branchless-Version ist über die verschiedenen Szenarien hinweg ziemlich stabil. Der ternäre ist in den betrachteten Fällen durchweg besser als der verzweigte, insbesondere wenn der Verzweigungsprädiktor einsetzt. Schließlich der
obwohl sie allgemeiner und erheblich langsamer ist, hat Chancen, optimiert zu werden, um die schnellste zu werden, wie im Fall bestimmter const-Werte auf der rechten Seite.
Natürlich ist dies völlig plattformabhängig, wer weiß, wie das auf einem Arduino sein wird 🙂
Wenn ‘len’ in Ihrem Code groß genug ist, ist die Bedingung schneller, da die Verzweigungsprädiktoren fast immer richtig raten.
#include <stdio.h>
#include <stdlib.h>
#define modulo
int main()
{
int iterations = 1000000000;
int size = 16;
int a[size];
unsigned long long res = 0;
int i, j;
for (i=0;i<size;i++)
a[i] = i;
for (i=0,j=0;i<iterations;i++)
{
j++;
#ifdef modulo
j %= size;
#else
if (j >= size)
j = 0;
#endif
res += a[j];
}
printf("%llu\n", res);
}
Wenn nicht, dann glaube ich, dass dies eng mit zirkulären Warteschlangen verbunden ist, bei denen es oft der Fall ist, dass die Länge eine Potenz von 2 ist. Dies ermöglicht es dem Compiler, Modulo durch ein einfaches UND zu ersetzen.
Der Code ist folgender:
Größe=15:
Modulo: 4.868 s
Bed.: 1.291s
Größe=16:
Modulo: 1.067 s
Bed.: 1.599s
Kompiliert in gcc 7.3.0 mit -O3-Optimierung. Das Gerät ist ein i7 920.
Ich frage mich, warum die Zeit der Cond-Version in beiden Fällen nicht gleich ist.
– Doug Currie
17. April 2019 um 14:35 Uhr
Ich denke, da res nicht flüchtig ist, kann gcc viele Optimierungen vornehmen, die mit zunehmender Größe weniger effektiv sind. Wenn ich ‘volatile’ zu res hinzufüge, betragen die Zeiten für die Bedingung immer etwa 2 Sekunden. Für Modulo etwa 2 Sek. bei Potenz von 2 und ansonsten nicht stabil (über 4 Sek., zunehmend mit der Größe).
– J. Reh
17. April 2019 um 15:05 Uhr
Mir ist auch aufgefallen, dass im Fall von nicht flüchtigen Res für die Größe 1024 die Bedingung schneller läuft, in 1 Sekunde, also denke ich, dass es um „gute“ und „schlechte“ Größen für die Optimierungen (oder Verzweigungsprädiktoren?) geht.
17. April 2019 um 15:14 Uhr
Benutzeravatar von steviekm3
steviekm3
Ich habe den Artikel über das Erstellen einer schnellen Hash-Karte gelesen. Ein Flaschenhals kann der Modulo-Operator sein, um den Hash-Bucket zu finden. Sie schlugen vor, Ihre Anzahl von Eimern zu einer Potenz von 2 zu machen. Anscheinend bedeutet Modul mit Potenz von zwei genau so, als würde man die letzten n Bits betrachten.
Ich frage mich, warum die Zeit der Cond-Version in beiden Fällen nicht gleich ist.
– Doug Currie
17. April 2019 um 14:35 Uhr
Ich denke, da res nicht flüchtig ist, kann gcc viele Optimierungen vornehmen, die mit zunehmender Größe weniger effektiv sind. Wenn ich ‘volatile’ zu res hinzufüge, betragen die Zeiten für die Bedingung immer etwa 2 Sekunden. Für Modulo etwa 2 Sek. bei Potenz von 2 und ansonsten nicht stabil (über 4 Sek., zunehmend mit der Größe).
– J. Reh
17. April 2019 um 15:05 Uhr
Mir ist auch aufgefallen, dass im Fall von nicht flüchtigen Res für die Größe 1024 die Bedingung schneller läuft, in 1 Sekunde, also denke ich, dass es um „gute“ und „schlechte“ Größen für die Optimierungen (oder Verzweigungsprädiktoren?) geht.
17. April 2019 um 15:14 Uhr
Benutzeravatar von Yilmaz
(i + 1) % len
Yilmaz
if ((i+1)==len){
return 0
} else {
return i+1
}
Der Modulo-Operator ist teuer, aber die Division ist auch teuer. Das Konvertieren Ihres Codes von der Verwendung des Modulo-Operators in die Division wird Ihren Code also nicht optimieren.
Um den obigen Code zu optimieren
Ich glaube, Sie haben den ternären Operator mit Division verwechselt. Die eigene Optimierung des Autors entspricht der von Ihnen vorgeschlagenen.
14148100cookie-checkIst es besser, den Mod-Operator möglichst zu vermeiden?yes
Da fehlt vielleicht der Wald vor lauter Bäumen.
– Burhan Khalid
24. März 2013 um 7:58 Uhr
wenn
len
ist eine Kompilierzeitkonstante eines aktuellen GCC-Compilers (mit-02
) macht normalerweise clevere Dinge und vermeidet oft die Modulus-Maschinenanweisung des Zielprozessors.– Basile Starynkevitch
24. März 2013 um 7:59 Uhr
Das ist wirklich die Art von Optimierung, die Sie vergessen sollten. Der optimierende Compiler wird es besser machen, als Sie es könnten. Viel wichtiger ist die Lesbarkeit Ihres Codes.
– Basile Starynkevitch
24. März 2013 um 8:04 Uhr
Oder Sie könnten das Array 1 länger machen und das erste Element in das neue letzte Element kopieren, damit Sie normal darauf zugreifen können. Je nach Umständen kann jede dieser drei Optionen die schnellste sein.
– Harald
24. März 2013 um 12:08 Uhr
Dies wird normalerweise in kreisförmigen Warteschlangen verwendet
– Mohamed El-Nakeep
13. Dezember 2013 um 19:29 Uhr