it-swarm.com.de

Warum kann (oder kann) der Compiler eine vorhersagbare Additionsschleife nicht zu einer Multiplikation optimieren?

Dies ist eine Frage, die mir beim Lesen der brillanten Antwort von Mysticial auf die Frage in den Sinn kam: Warum kann ein sortiertes Array schneller verarbeitet werden als ein unsortiertes Array ?

Kontext für die beteiligten Typen:

const unsigned arraySize = 32768;
int data[arraySize];
long long sum = 0;

In seiner Antwort erklärt er, dass der Intel Compiler (ICC) dies optimiert:

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (data[c] >= 128)
            sum += data[c];

... in etwas Äquivalent dazu:

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

Der Optimierer erkennt, dass diese gleichwertig sind, und wechselt daher die Schleifen und bewegt den Zweig außerhalb der inneren Schleife. Sehr schlau!

Aber warum macht es das nicht?

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

Hoffentlich kann Mysticial (oder jemand anderes) eine ebenso brillante Antwort geben. Ich habe noch nie von den in dieser anderen Frage besprochenen Optimierungen erfahren, daher bin ich wirklich dankbar.

112
jhabbott

Der Compiler kann nicht generell umwandeln

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

in

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000 * data[c];

weil letzteres zu einem Überlauf von vorzeichenbehafteten ganzen Zahlen führen kann, wo erstere nicht möglich ist. Selbst bei garantiertem Wrap-around-Verhalten für den Überlauf von Zweierkomplement-Ganzzahlen mit Vorzeichen würde sich das Ergebnis ändern (wenn data[c] 30000 ist, würde das Produkt -1294967296 für die typischen 32-Bit-Variablen ints mit Wrap around werden, während 100000-mal 30000 zu sum hinzufügen würde Wenn dies nicht überläuft, erhöhen Sie sum um 3000000000). Beachten Sie, dass dies auch für vorzeichenlose Mengen gilt, bei unterschiedlichen Nummern. Ein Überlauf von 100000 * data[c] führt in der Regel zu einem Reduzierungsmodul 2^32, das im Endergebnis nicht vorkommen darf.

Es könnte daraus werden

for (int c = 0; c < arraySize; ++c)
    if (data[c] >= 128)
        sum += 100000LL * data[c];  // resp. 100000ull

wenn jedoch, wie üblich, long long ausreichend größer als int ist.

Warum es das nicht tut, kann ich nicht sagen, ich denke es ist das, was Mysticial sagte , "anscheinend führt es keinen Loop-kollabierenden Durchlauf nach dem Loop-Interchange durch".

Beachten Sie, dass der Loop-Interchange selbst generell nicht gültig ist (für signierte Ganzzahlen), da

for (int c = 0; c < arraySize; ++c)
    if (condition(data[c]))
        for (int i = 0; i < 100000; ++i)
            sum += data[c];

wo kann es zu Überlauf kommen

for (int i = 0; i < 100000; ++i)
    for (int c = 0; c < arraySize; ++c)
        if (condition(data[c]))
            sum += data[c];

würde nicht Es ist hier koscher, da die Bedingung dafür sorgt, dass alle hinzugefügten data[c] dasselbe Zeichen haben. Wenn also eines überläuft, tun dies beide.

Ich bin mir nicht sicher, ob der Compiler dies berücksichtigt hat (@Mysticial, könnten Sie es mit einer Bedingung wie data[c] & 0x80 versuchen oder so, dass dies für positive und negative Werte zutrifft?). Ich hatte Compiler ungültige Optimierungen vornehmen lassen (z. B. hatte ich vor einigen Jahren einen ICC (11.0, iirc) verwendet, der die signierte-32-Bit-int-to-double-Konvertierung in 1.0/n verwendete, wobei n ein unsigned int war schnell wie die Ausgabe von gcc. Aber falsch, viele Werte waren größer als 2^31, oops.).

89
Daniel Fischer

Diese Antwort gilt nicht für den konkreten Fall, der verlinkt wird. Sie gilt jedoch für den Fragetitel und kann für zukünftige Leser interessant sein:

Aufgrund der endlichen Genauigkeit ist die wiederholte Gleitkommaaddition nicht gleichbedeutend mit der Multiplikation. Erwägen:

float const step = 1e-15;
float const init = 1;
long int const count = 1000000000;

float result1 = init;
for( int i = 0; i < count; ++i ) result1 += step;

float result2 = init;
result2 += step * count;

cout << (result1 - result2);

Demo: http://ideone.com/7RhfP

44
Ben Voigt

Der Compiler enthält verschiedene Durchläufe, die die Optimierung übernehmen. Normalerweise werden in jedem Durchlauf entweder eine Optimierung von Anweisungen oder Schleifenoptimierungen durchgeführt. Derzeit gibt es kein Modell, das eine Optimierung des Schleifenkörpers basierend auf den Schleifenköpfen vornimmt. Dies ist schwer zu erkennen und seltener. 

Die Optimierung, die durchgeführt wurde, war die schleifeninvariante Codebewegung. Dies kann mit einer Reihe von Techniken erfolgen.

5
knightrider

Nun, ich würde vermuten, dass einige Compiler diese Art der Optimierung durchführen könnten, vorausgesetzt, wir sprechen von Integer-Arithmetik.

Gleichzeitig lehnen einige Compiler dies möglicherweise ab, da das Ersetzen wiederholter Additionen durch Multiplikationen das Überlaufverhalten des Codes ändern kann. Bei unsigned-Integraltypen sollte es keinen Unterschied machen, da ihr Überlaufverhalten vollständig von der Sprache festgelegt wird. Aber für signierte könnte es sein (wahrscheinlich nicht auf der Komplement-Plattform von 2). Es ist wahr, dass ein signierter Überlauf tatsächlich zu undefiniertem Verhalten in C führt, was bedeutet, dass es völlig in Ordnung sein sollte, die Überlaufsemantik insgesamt zu ignorieren. Nicht alle Compiler sind mutig genug, dies zu tun. Es zieht häufig Kritik aus der Menge "C ist nur eine höhere Versammlungssprache" an. (Denken Sie daran, was passiert ist, als der GCC Optimierungen auf der Basis von Strikt-Aliasing-Semantik eingeführt hat?)

In der Vergangenheit hat sich GCC als Compiler erwiesen, der über die notwendigen Schritte verfügt, um solche drastischen Schritte zu unternehmen. Andere Compiler ziehen es jedoch vor, das wahrgenommene "vom Benutzer beabsichtigte" Verhalten beizubehalten, auch wenn es von der Sprache nicht definiert wird.

3
AnT

Bei dieser Art der Optimierung gibt es ein konzeptionelles Hindernis. Compiler-Autoren geben sich viel Mühe, um die Stärke zu reduzieren - zum Beispiel, um Multiplikationen durch Additionen und Verschiebungen zu ersetzen. Sie gewöhnen sich daran zu denken, dass Multiplikationen schlecht sind. Ein Fall, in dem man in die andere Richtung gehen sollte, ist daher überraschend und nicht intuitiv. Also denkt niemand daran, es umzusetzen.

3
zwol

Die Leute, die Compiler entwickeln und warten, haben nur eine begrenzte Menge an Zeit und Energie für ihre Arbeit. Daher möchten sie sich in der Regel auf das konzentrieren, was ihre Benutzer am meisten interessieren: aus gut geschriebenem Code wird schnell Code. Sie möchten nicht ihre Zeit damit verbringen, nach Wegen zu suchen, wie aus dummem Code ein schneller Code wird - dafür gibt es die Überprüfung des Codes. In einer Hochsprache kann es "dummen" Code geben, der eine wichtige Idee ausdrückt, was es den Entwicklern wert macht, dies so schnell wie möglich zu machen. Zum Beispiel ermöglichen die Abkürzung der Entwaldung und die Stream-Fusion Haskell-Programmen, die um bestimmte Arten von Faulheit herum angeordnet sind produzierte Datenstrukturen, die in enge Schleifen kompiliert werden sollten, die keinen Speicher zuweisen. Diese Art von Anreiz gilt jedoch einfach nicht für die Umwandlung von Schleifen in Multiplikationen. Wenn Sie möchten, dass es schnell ist, schreiben Sie es einfach mit Multiplikation.

0
dfeuer