Dlaczego mój program jest powolny, gdy zapętla dokładnie 8192 elementy?

Oto wyciąg z omawianego programu. Macierz img[][] ma rozmiar SIZE×SIZE i jest inicjalizowana w:

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

Następnie tworzy się macierz res[][], a każde pole tutaj ma być średnią z 9 pól wokół niego w macierzy img. Granica jest pozostawiona na 0 dla uproszczenia.

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;
}
To wszystko w programie. Dla kompletności, oto, co było wcześniej. Żaden kod nie pojawia się później. Jak widzisz, to tylko inicjalizacja.
#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;

Zasadniczo ten program jest wolny, gdy rozmiar jest wielokrotnością 2048, np. czas wykonania:

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

Kompilatorem jest GCC. Z tego, co wiem, wynika to z zarządzania pamięcią, ale nie wiem zbyt wiele na ten temat, dlatego pytam tutaj.

Również jak to naprawić byłoby miło, ale gdyby ktoś mógł wyjaśnić te czasy realizacji, byłbym już wystarczająco szczęśliwy.

Znam już malloc / free, ale problem nie jest ilość pamięci zużyta, to tylko czas wykonania, więc nie wiem jak to by pomogło.

Author: casperOne, 2012-09-04

3 answers

Różnica jest spowodowana tym samym problemem super-wyrównania z następujących powiązanych pytań:

Ale to tylko dlatego, że jest jeszcze jeden problem z kodem.

Począwszy od oryginalnej pętli:

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;
}

Najpierw zauważ, że dwa pętle wewnętrzne są trywialne. Można je rozwinąć w następujący sposób:

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;
    }
}
Więc zostawiamy dwie zewnętrzne pętle, którymi jesteśmy zainteresowani.

Teraz widzimy, że problem jest taki sam w tym pytaniu: dlaczego kolejność pętli wpływa na wydajność podczas iteracji na tablicy 2D?

Iterujesz macierz w kolumnie zamiast w wierszu.


Aby rozwiązać ten problem, należy wymienić dwie pętle.

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;
    }
}

To eliminuje wszystkie bez sekwencyjny dostęp całkowicie, więc nie masz już losowych spowolnień na dużych mocach dwóch.


Core i7 920 @ 3.5 GHz

Oryginalny kod:

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

Wymienne Zewnętrzne Pętle:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds
 882
Author: Mysticial,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 10:31:30

Poniższe testy zostały wykonane z kompilatorem Visual C++, ponieważ jest on używany przez domyślną instalację Qt Creator (chyba bez flagi optymalizacji). Podczas korzystania z GCC nie ma dużej różnicy między wersją Mystic a moim" zoptymalizowanym " kodem. Wniosek jest taki, że optymalizacje kompilatorów dbają o mikro optymalizację lepiej niż ludzie(w końcu ja). Resztę odpowiedzi zostawiam w celach informacyjnych.


Nie jest wydajne przetwarzanie obrazów w ten sposób. Lepiej używać single tablice wymiarów. Przetwarzanie wszystkich pikseli odbywa się w jednej pętli. Losowy dostęp do punktów można uzyskać za pomocą:
pointer + (x + y*width)*(sizeOfOnePixel)

W tym konkretnym przypadku lepiej obliczyć i buforować sumę trzech grup pikseli poziomo, ponieważ są one używane trzy razy każda.

Zrobiłem kilka testów i myślę, że warto się nimi podzielić. Każdy wynik to średnio pięć testów.

Oryginalny kod użytkownika user1615209:

8193: 4392 ms
8192: 9570 ms

Wersja mistyczna:

8193: 2393 ms
8192: 2190 ms

Dwa przejścia przy użyciu tablicy 1D: pierwsze przejście dla Sum poziomych, drugie dla Sum pionowych i średnich. Adresowanie dwujezdniowe z trzema wskaźnikami i tylko przyrostami w ten sposób:

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

Dwa przejścia używając tablicy 1D i adresując Tak:

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

8193: 932 ms
8192: 925 ms

One pass caching horizontal sumuje tylko jeden wiersz do przodu, więc pozostają w pamięci podręcznej:

// 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

Wniosek:

  • brak korzyści z używania kilku wskaźników i tylko przyrostów (myślałem, że będzie szybciej)
  • buforowanie Sum poziomych jest lepsze niż obliczam je kilka razy.
  • dwa przejścia nie są trzy razy szybsze, tylko dwa razy.
  • Możliwe jest osiągnięcie 3,6 razy szybciej, zarówno przy użyciu pojedynczego przejścia, jak i buforowania pośredniego wyniku.]}

Jestem pewien, że można zrobić znacznie lepiej.

Uwaga Proszę zauważyć, że napisałem tę odpowiedź na ogólne Problemy z wydajnością, a nie problem pamięci podręcznej wyjaśniony w doskonałej odpowiedzi Mystic. Na początku był to tylko pseudo kod. Poproszono mnie o zrób testy w komentarzach... Oto całkowicie zreformowana wersja z testami.

 52
Author: bokan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-20 07:08:42

Uporządkowanie dostępu do elementu zostało jeszcze kilka nisko wiszących owoców. Akumulacji można dokonać w taki sposób, że podczas iteracji w prawo tylko 3 nowe wartości muszą być pobrane z pamięci i nagromadzone. Sztuczka polega na tym, aby wiedzieć, jak upuścić lewą kolumnę; podczas dodawania nowej kolumny zapamiętaj jej wartość, dopóki nie wyjdzie z okna próbkowania.

Koszt przed: 9 Czytaj, 9 dodawaj, 1 dział Koszt po: 3 read, 3 addition, 1 division

Think of the okno próbkowania jako pole 3x3, w którym śledzisz każdą kolumnę (1x3) osobno. Zbierz nową kolumnę i upuść najstarszą.

Dzielenie jest instrukcją o dużym opóźnieniu, więc może być korzystne ukrycie opóźnienia, ale przed pójściem tam wyjście kompilatora powinno być sprawdzane, czy dzielenie przez stałą jest elided i jeśli rozwijanie pętli (przez kompilator) już wykonuje pewną kompensację opóźnienia.

Ale po najbardziej dramatycznej optymalizacji poprawnego użycia pamięci podręcznej to naprawdę drobne rzeczy.

 -1
Author: t0rakka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-10-11 12:08:18