797 Stimmen

Warum ist mein Programm langsam, wenn es in einer Schleife über genau 8192 Elemente läuft?

Hier ist der Auszug aus dem betreffenden Programm. Die Matrix img[][] hat die Größe SIZE×SIZE und wird mit initialisiert:

img[j][i] = 2 * j + i

Dann erstellen Sie eine Matrix res[][] und jedes Feld hier ist der Durchschnitt der 9 umliegenden Felder in der img-Matrix. Der Rand wird der Einfachheit halber bei 0 belassen.

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Das ist alles, was das Programm ausmacht. Der Vollständigkeit halber, hier ist das, was davor kommt. Danach kommt kein Code mehr. Wie Sie sehen können, ist es nur eine Initialisierung.

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

Grundsätzlich ist dieses Programm langsam, wenn SIZE ein Vielfaches von 2048 ist, z.B. bei den Ausführungszeiten:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

Der Compiler ist GCC. Soweit ich weiß, ist dies wegen der Speicherverwaltung, aber ich weiß nicht wirklich zu viel über dieses Thema, weshalb ich hier frage.

Auch wie man das beheben kann, wäre schön, aber wenn mir jemand diese Ausführungszeiten erklären könnte, wäre ich schon glücklich genug.

Ich kenne bereits malloc/free, aber das Problem ist nicht die Menge des verwendeten Speichers, sondern lediglich die Ausführungszeit, daher weiß ich nicht, wie das helfen würde.

1008voto

Mysticial Punkte 451718

Der Unterschied wird durch dasselbe Problem der Superausrichtung verursacht, das auch in den folgenden Fragen angesprochen wird:

Das liegt aber nur daran, dass es ein weiteres Problem mit dem Code gibt.

Ausgehend von der ursprünglichen Schleife:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Zunächst fällt auf, dass die beiden inneren Schleifen trivial sind. Sie können wie folgt abgerollt werden:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Es bleiben also die beiden äußeren Schleifen, die uns interessieren.

Jetzt sehen wir, dass das Problem in dieser Frage dasselbe ist: Warum beeinflusst die Reihenfolge der Schleifen die Leistung bei der Iteration über ein 2D-Array?

Sie iterieren die Matrix spaltenweise statt zeilenweise.


Um dieses Problem zu lösen, sollten Sie die beiden Schleifen vertauschen.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Dadurch wird der nichtsequenzielle Zugriff vollständig eliminiert, so dass bei großen Zweierpotenzen keine zufälligen Verlangsamungen mehr auftreten.


Core i7 920 @ 3,5 GHz

Ursprünglicher Code:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Vertauschte äußere Schleifen:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

59voto

bokan Punkte 3489

Die folgenden Tests wurden mit dem Visual C++ Compiler durchgeführt, wie er von der Standardinstallation von Qt Creator verwendet wird (ich vermute, ohne Optimierungsflag). Bei der Verwendung von GCC gibt es keinen großen Unterschied zwischen der Version von Mystical und meinem "optimierten" Code. Die Schlussfolgerung ist also, dass Compiler-Optimierungen sich besser um die Mikro-Optimierung kümmern als Menschen (letztendlich ich). Ich lasse den Rest meiner Antwort als Referenz stehen.


Es ist nicht effizient, Bilder auf diese Weise zu verarbeiten. Es ist besser, eindimensionale Arrays zu verwenden. Die Verarbeitung aller Pixel wird in einer Schleife durchgeführt. Der zufällige Zugriff auf Punkte könnte mit:

pointer + (x + y*width)*(sizeOfOnePixel)

In diesem speziellen Fall ist es besser, die Summe von drei Pixelgruppen horizontal zu berechnen und zwischenzuspeichern, da sie jeweils dreimal verwendet werden.

Ich habe einige Tests durchgeführt und denke, es lohnt sich, sie zu teilen. Jedes Ergebnis ist ein Durchschnitt von fünf Tests.

Originalcode von Benutzer1615209:

8193: 4392 ms
8192: 9570 ms

Die Version von Mystical:

8193: 2393 ms
8192: 2190 ms

Zwei Durchläufe mit einem 1D-Array: erster Durchlauf für horizontale Summen, zweiter Durchlauf für vertikale Summe und Durchschnitt. Adressierung in zwei Durchgängen mit drei Zeigern und nur Inkrementen wie hier:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

Zwei Durchgänge mit einem 1D-Array und einer Adressierung wie folgt:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Horizontale Summen, die in einem Durchgang gecacht werden, sind nur eine Zeile voraus, damit sie im Cache bleiben:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Schlussfolgerung:

  • Keine Vorteile der Verwendung mehrerer Zeiger und nur Inkremente (ich dachte, es wäre schneller gewesen)
  • Das Zwischenspeichern horizontaler Summen ist besser, als sie mehrmals zu berechnen.
  • Zwei Durchgänge sind nicht dreimal schneller, sondern nur zweimal.
  • Mit einem einzigen Durchgang und der Zwischenspeicherung eines Zwischenergebnisses lässt sich eine 3,6-fache Geschwindigkeit erzielen.

Ich bin mir sicher, dass man es noch viel besser machen kann.

ANMERKUNG Bitte beachten Sie, dass ich diese Antwort geschrieben habe, um allgemeine Leistungsprobleme zu behandeln und nicht das Cache-Problem, das in der ausgezeichneten Antwort von Mystical erläutert wird. Am Anfang war es nur Pseudo-Code. In den Kommentaren wurde ich gebeten, Tests durchzuführen... Hier ist eine vollständig umgestaltete Version mit Tests.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X