2015-12-15 15 views
10
#include <stdio.h> 
#include <time.h> 

#define N 32768 

char a[N][N]; 
char b[N][N]; 

int main() { 
    int i, j; 

    printf("address of a[%d][%d] = %p\n", N, N, &a[N][N]); 
    printf("address of b[%5d][%5d] = %p\n", 0, 0, &b[0][0]); 

    clock_t start = clock(); 
    for (j = 0; j < N; j++) 
     for (i = 0; i < N; i++) 
      a[i][j] = b[i][j]; 
    clock_t end = clock(); 
    float seconds = (float)(end - start)/CLOCKS_PER_SEC; 
    printf("time taken: %f secs\n", seconds); 

    start = clock(); 
    for (i = 0; i < N; i++) 
     for (j = 0; j < N; j++) 
      a[i][j] = b[i][j]; 
    end = clock(); 
    seconds = (float)(end - start)/CLOCKS_PER_SEC; 
    printf("time taken: %f secs\n", seconds); 

    return 0; 
} 

wyjściowa:Dlaczego kopiowanie kolumny tablicy 2D według kolumny trwa dłużej niż wiersz po wierszu w C?

address of a[32768][32768] = 0x80609080 
address of b[ 0][ 0] = 0x601080 
time taken: 18.063229 secs 
time taken: 3.079248 secs 

Dlaczego kolumna przez kopiowanie kolumny wziąć prawie 6 razy tak długo, jak rząd przez kopiowanie wierszy? Rozumiem, że tablica 2D to w zasadzie tablica o rozmiarach nxn, gdzie A [i] [j] = A [i * n + j], ale przy użyciu prostej algebry, obliczyłem, że głowica maszyny Turinga (w pamięci głównej) musiałaby podróżować odległość w obu przypadkach: enter image description here. Tutaj nxn jest rozmiarem tablicy, a x jest odległością między ostatnim elementem pierwszej tablicy a pierwszym elementem drugiej tablicy.

+3

Dlaczego obliczasz czasy pracy na podstawie ruchów głową maszyny Turinga? W nowoczesnej pamięci RAM komputera nie ma ruchomej głowicy odczytu i zapisu. – user2357112

+0

podstawowa pamięć bębna ... stare dobre czasy! – chqrlie

Odpowiedz

14

To dość dużo sprowadza się do tego obrazu (source):

enter image description here

Podczas dostępu do danych, procesor nie będzie załadować tylko jedną wartość, ale również załadować sąsiednie dane do L1 cache CPU . Podczas iteracji po tablicy w kolejności elementy, które zostały automatycznie załadowane do pamięci podręcznej, są w rzeczywistości tymi, które są przetwarzane w następnej kolejności. Jednak podczas iteracji kolumnowej, za każdym razem, gdy załadowana jest cała "linia pamięci podręcznej" danych (rozmiar zmienia się na procesor), używany jest tylko jeden element, a następnie musi zostać załadowany następny wiersz, co skutecznie czyni cache bezcelowym .

Model wikipedia entry i jako przegląd wysokiego poziomu, this PDF powinien pomóc w zrozumieniu działania pamięci podręcznej procesora.

Edytuj: chqrlie w komentarzach jest oczywiście poprawny. Jednym z istotnych czynników jest to, że tylko nieliczne z twoich kolumn mieszczą się w pamięci podręcznej L1 w tym samym czasie. Jeśli twoje wiersze były znacznie mniejsze (powiedzmy, całkowity rozmiar macierzy dwuwymiarowej wynosił tylko kilka kilobajtów), możesz nie zauważyć wpływu wydajności na iterację w kolumnie.

+3

To jest prawidłowe wyjaśnienie, ale trzeba wyjaśnić, dlaczego dane są ponownie ładowane wiele razy, ponieważ liczba wierszy i kolumn jest taka, że ​​dane nie mieszczą się w pamięci podręcznej L1. – chqrlie

+1

Po zmianie tablic na char a [N] [N + 1]; char b [N] [N + 1]; Kolumna po kolumnie staje się szybsza (trwa około 10 sekund). Dlaczego tak jest? – Kakaji

+2

@ user3711976, ze względu na problemy z wyrównaniem pamięci podręcznej - http://stackoverflow.com/questions/11413855/why-is-transposing-a-matrix-of-512x512-co- wolniej -transpozycja-a-macierz- – Leeor

4

Podczas gdy normalne jest rysowanie tablicy jako prostokąta, adresowanie elementów tablicy w pamięci jest liniowe: od 0 do jednego minus liczba dostępnych bajtów (na prawie wszystkich maszynach).

hierarchii pamięci (na przykład pamięć podręczna L1 rejestruje < < pamięci podręcznej L2 < RAM < obszaru wymiany na dysku) są optymalizowane dla przypadku, w którym zlokalizowane są dostępy do pamięci: dostępów, które są kolejne w adresach czasowych dotykowe, które są blisko siebie. Są one jeszcze bardziej zoptymalizowane (na przykład ze strategiami pre-fetch) dla sekwencyjnego dostępu w liniowej kolejności adresów; na przykład 100,101,102 ...

W języku C tablice prostokątne są ułożone w porządku liniowym przez połączenie wszystkich wierszy (zamiast tego można użyć innych języków, takich jak kolumny łączące FORTRAN i Common Lisp). Dlatego najbardziej efektywnym sposobem na odczytanie lub zapisanie tablicy jest wykonanie wszystkich kolumn pierwszego rzędu, a następnie przejście do pozostałych, wiersz po wierszu.

Jeśli zamiast tego przejdziesz w dół kolumn, kolejne dotknięcia będą wynosić N bajtów, gdzie N to liczba bajtów z rzędu: 100, 10100, 20100, 30100 ... dla przypadku N = 10000 bajtów. Następnie druga kolumna to 101010101, 20101 itd. Jest to absolutnie najgorszy przypadek dla większości schematów pamięci podręcznej.

W najgorszym przypadku możesz spowodować błąd strony przy każdym dostępie. Te dni nawet na przeciętnej maszynie wymagałyby ogromnej tablicy. Ale jeśli tak się stanie, każdy dotyk może kosztować około 10 ms dla szukania głowy. Dostęp sekwencyjny wynosi kilka nan na sekundę. To ponad czynnik o wartości miliona różnicę. Obliczenie skutecznie zatrzymuje się w tym przypadku. Ma nazwę: dysk wyrzuca.

W bardziej normalnym przypadku, w którym występują tylko błędy pamięci podręcznej, a nie błędy strony, może pojawić się współczynnik wynoszący sto. Nadal warte uwagi.

1

są 3 główne aspekty, które przyczyniają się do czasu innego:

  1. pierwszej podwójnej pętli dostęp zarówno macierze po raz pierwszy. W rzeczywistości czytasz niezainicjowaną pamięć, która jest zła, jeśli oczekujesz znaczących rezultatów (zarówno pod względem funkcjonalnym, jak i pod względem czasowym), ale pod względem czasu, który odgrywa tu rolę, jest to, że adresy te są zimne i znajdują się w pamięci głównej (jeśli masz szczęście) lub nie jesteś nawet stronicowany (jeśli masz mniej szczęścia). W tym drugim przypadku wystąpiłaby usterka strony na każdej nowej stronie i wywołałaby wywołanie systemowe, aby przydzielić stronę po raz pierwszy. Zauważ, że nie ma to nic wspólnego z kolejnością przemierzania, ale po prostu dlatego, że pierwszy dostęp jest znacznie wolniejszy. Aby tego uniknąć, zainicjuj obie tablice do pewnej wartości.

  2. Lokalizacja linii pamięci podręcznej (jak wyjaśniono w innych odpowiedziach) - jeśli uzyskujesz dostęp do danych sekwencyjnych, tracisz jeden raz na wiersz, a następnie czerpiesz korzyści związane z jego pobieraniem. Najprawdopodobniej nie trafisz nawet w pamięć podręczną, ale raczej w bufor, ponieważ kolejne żądania będą oczekiwać na pobranie tej linii. Podczas uzyskiwania dostępu do kolumn, pobierasz linię, buforujesz ją, ale jeśli odległość do ponownego wykorzystania jest wystarczająco duża - stracisz ją i będziesz musiał pobrać ją ponownie.

  3. Wstępne pobieranie - nowoczesne procesory miałyby mechanizmy preselekcji HW, które mogą wykrywać sekwencyjne dostępy i wstępnie pobierać dane z wyprzedzeniem, co wyeliminuje nawet pierwszą brak każdej linii. Większość procesorów ma również wsteczne, które mogą być w stanie pokryć rozmiar kolumny, ale te rzeczy nie działają dobrze z zazwyczaj strukturami matrycy, ponieważ masz zbyt wiele kolumn i byłoby niemożliwe, aby HW śledził wszystkie te strumienie kroku jednocześnie.

Na marginesie, zaleciłbym, aby pomiar czasu był wykonywany wiele razy i amortyzowany - to wyeliminowałoby problem nr 1.

Powiązane problemy