Dlaczego kolejność pętli wpływa na wydajność podczas iteracji na tablicy 2D?

Możliwy duplikat:
która z tych dwóch pętli for jest bardziej efektywna pod względem czasu i wydajności pamięci podręcznej

Poniżej znajdują się dwa programy, które są prawie identyczne z tym, że zamieniłem zmienne i i j wokół. Obie biegną w różnym czasie. Czy ktoś może wyjaśnić, dlaczego tak się dzieje?

Wersja 1

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (i = 0; i < 4000; i++) {
    for (j = 0; j < 4000; j++) {
      x[j][i] = i + j; }
  }
}

Wersja 2

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (j = 0; j < 4000; j++) {
     for (i = 0; i < 4000; i++) {
       x[j][i] = i + j; }
   }
}
Author: Community, 2012-03-30

7 answers

Jak mówili inni, problem polega na przechowywaniu pamięci w tablicy: x[i][j]. Oto mały wgląd dlaczego:

Macie macierz 2-wymiarową, ale pamięć w komputerze jest z natury 1-wymiarowa. Więc wyobraź sobie swoją tablicę w ten sposób:

0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3

Twój komputer przechowuje go w pamięci jako pojedynczą linię:

0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3

W drugim przykładzie uzyskujesz dostęp do tablicy przez zapętlenie nad drugą liczbą pierwszą, tj.:

x[0][0] 
        x[0][1]
                x[0][2]
                        x[0][3]
                                x[1][0] etc...

Co oznacza, że uderzasz je wszystkie w spokój. Teraz spójrz na 1. wersję. Robisz:

x[0][0]
                                x[1][0]
                                                                x[2][0]
        x[0][1]
                                        x[1][1] etc...

Ze względu na sposób, w jaki C rozłożył tablicę 2-d w pamięci, prosisz ją, aby przeskoczyła po całym miejscu. Ale teraz Kopacz: dlaczego to ma znaczenie? Dostęp do pamięci jest taki sam, prawda?

Nie: z powodu pamięci podręcznej. Dane z pamięci są przesyłane do procesora w małych kawałkach (zwanych "liniami pamięci podręcznej"), Zwykle 64 bajtów. Jeśli masz 4-bajtowe liczby całkowite, oznacza to, że otrzymujesz 16 kolejnych liczb całkowitych w zgrabnym małym / align = "left" / Pobieranie tych kawałków pamięci jest w rzeczywistości dość powolne; procesor może wykonać wiele pracy w czasie ładowania pojedynczej linii pamięci podręcznej.

Teraz spójrz wstecz na kolejność dostępu: drugim przykładem jest (1) pobranie fragmentu 16 intów, (2) modyfikacja wszystkich z nich, (3) powtórzenie 4000*4000/16 razy. To jest ładne i szybkie, a procesor zawsze ma coś do pracy.

Pierwszy przykład to (1) chwyć kawałek 16 intów, (2) zmodyfikuj tylko jeden z nich, (3) Powtórz 4000*4000 razy. To będzie wymagało 16 razy więcej "pobrań" z pamięci. Procesor będzie musiał spędzać czas siedząc i czekając, aż pojawi się ta pamięć, a podczas siedzenia marnujesz cenny czas.

Ważna Uwaga:

Teraz, gdy masz odpowiedź, oto interesująca uwaga: nie ma nieodłącznego powodu, że twój drugi przykład musi być szybki. Na przykład w Fortranie pierwszy przykład byłby szybki, a drugi powolny. To dlatego, że zamiast rozszerzać rzeczy na koncepcyjne "wiersze", tak jak robi to C, Fortran rozszerza się na "kolumny", tj.:

0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3

Układ C nazywa się' row-major', A Fortran's nazywa się ' column-major'. Jak widać, bardzo ważne jest, aby wiedzieć, czy twój język programowania jest wierszem-dur czy kolumną-dur! Oto link po więcej informacji: http://en.wikipedia.org/wiki/Row-major_order

 536
Author: Robert Martin,
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
2016-05-07 15:28:52

Nic wspólnego z montażem. Jest to spowodowane pominięciem pamięci podręcznej .

C wielowymiarowe tablice są przechowywane z ostatnim wymiarem jako najszybszym. Tak więc pierwsza wersja będzie brakowało pamięci podręcznej na każdej iteracji, podczas gdy druga wersja nie będzie. więc druga wersja powinna być znacznie szybsza.

Zobacz też: http://en.wikipedia.org/wiki/Loop_interchange .

 62
Author: Oliver Charlesworth,
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
2012-03-30 02:20:03

Wersja 2 będzie działać znacznie szybciej, ponieważ używa pamięci podręcznej komputera lepiej niż wersja 1. Jeśli się nad tym zastanowić, tablice są tylko sąsiadującymi ze sobą obszarami pamięci. Gdy zażądasz elementu w tablicy, Twój system operacyjny prawdopodobnie wprowadzi stronę pamięci do pamięci podręcznej, która zawiera ten element. Ponieważ jednak kilka kolejnych elementów znajduje się również na tej stronie (ponieważ sąsiadują ze sobą), następny dostęp będzie już w pamięci podręcznej! To właśnie robi Wersja 2, aby przyspieszyć.

Wersja 1, z drugiej strony, jest dostęp do elementów mądre kolumny, a nie mądry wiersz. Ten rodzaj dostępu nie jest sąsiadujący z poziomem pamięci, więc program nie może korzystać z buforowania systemu operacyjnego tak bardzo.

 22
Author: Oleksi,
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
2012-03-30 02:21:45

Powodem jest Cache-local data access. W drugim programie skanujesz liniowo przez pamięć, która korzysta z buforowania i wstępnego ustawiania. Wzorzec użycia pamięci pierwszego programu jest znacznie bardziej rozłożony i dlatego ma gorsze zachowanie pamięci podręcznej.

 12
Author: Variable Length Coder,
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
2012-03-30 02:22:38

Oprócz innych doskonałych odpowiedzi na trafienia w pamięci podręcznej, istnieje również możliwa różnica w optymalizacji. Twoja druga pętla prawdopodobnie zostanie zoptymalizowana przez kompilator do czegoś równoważnego:

  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }

Jest to mniej prawdopodobne dla pierwszej pętli, ponieważ musiałaby zwiększyć wskaźnik " p " O 4000 za każdym razem.

Edytuj: p++ i nawet *p++ = .. może być skompilowana do jednej instrukcji procesora w większości procesorów. *p = ..; p += 4000 nie może, więc jest mniej korzyści w jej optymalizacji. On również trudniejsze, ponieważ kompilator musi znać i używać rozmiaru tablicy wewnętrznej. I nie występuje tak często w pętli wewnętrznej w normalnym kodzie (występuje tylko dla tablic wielowymiarowych, gdzie ostatni indeks jest utrzymywany na stałym poziomie W pętli, a przedostatni jest schodkowany), więc optymalizacja jest mniej priorytetowa.

 10
Author: fishinear,
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
2016-03-07 15:02:48

Ten wiersz winowajca:

x[j][i]=i+j;

Druga wersja wykorzystuje pamięć ciągłą, dzięki czemu będzie znacznie szybsza.

Próbowałem z

x[50000][50000];

I czas wykonania to 13s dla wersji1 vs 0.6 s dla wersji2.

 8
Author: Nicolas Modrzyk,
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
2012-03-30 02:29:24

staram się udzielić ogólnej odpowiedzi.

Ponieważ i[y][x] jest skrótem *(i + y*array_width + x) W C (wypróbuj classy int P[3]; 0[P] = 0xBEEF;).

Kiedy iterację nad y, iterację nad kawałkami wielkości array_width * sizeof(array_element). Jeśli masz to w swojej wewnętrznej pętli, wtedy będziesz miał array_width * array_height iteracje nad tymi kawałkami.

Odwracając kolejność, będziesz miał tylko array_height iteracje chunk, a pomiędzy dowolną iteracją chunk, będziesz miał array_width iteracje tylko sizeof(array_element).

Podczas gdy na naprawdę starym x86-Procesory to nie miało większego znaczenia, w dzisiejszych czasach' x86 robi dużo wstępnego ustawiania i buforowania danych. Prawdopodobnie produkujesz wiele braków pamięci podręcznej w swojej wolniejszej kolejności iteracji.

 3
Author: Sebastian Mach,
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
2012-03-30 15:20:15