2017-03-02 18 views
12

Poniższy przykład kodu generuje macierz o rozmiarze N i przenosi ją SAMPLES liczbę razy. Kiedy N = 512 średni czas wykonania operacji transpozycji to 2144 μs (coliru link). Na pierwszy rzut oka nie ma nic specjalnego, prawda? ...Dlaczego te czasy transpozycji macierzy są tak intuicyjne?

Cóż, tutaj znajdują się wyniki dla

  • N = 5131451 μs
  • N = 519600 μs
  • N = 530486 μs
  • N = 540492 μs (wreszcie! Teoria zacznie działać :).

Dlaczego więc w praktyce te proste obliczenia różnią się od teorii? Czy to zachowanie wiąże się ze spójnością pamięci podręcznej procesora lub brakami w pamięci podręcznej? Jeśli tak, proszę wyjaśnij.

#include <algorithm> 
#include <iostream> 
#include <chrono> 

constexpr int N  = 512; // Why is 512 specifically slower (as of 2016) 
constexpr int SAMPLES = 1000; 
using us = std::chrono::microseconds; 

int A[N][N]; 

void transpose() 
{ 
    for (int i = 0 ; i < N ; i++) 
    for (int j = 0 ; j < i ; j++) 
     std::swap(A[i][j], A[j][i]); 
} 

int main() 
{ 
    // initialize matrix 
    for (int i = 0 ; i < N ; i++) 
    for (int j = 0 ; j < N ; j++) 
     A[i][j] = i+j; 

    auto t1 = std::chrono::system_clock::now(); 
    for (int i = 0 ; i < SAMPLES ; i++) 
     transpose(); 
    auto t2 = std::chrono::system_clock::now(); 

    std::cout << "Average for size " << N << ": " << std::chrono::duration_cast<us>(t2 - t1).count()/SAMPLES << " (us)"; 
} 
+0

Ile razy uruchomiłeś ten fragment? Środowiska wykonawcze mogą znacznie różnić się w zależności od uruchomienia, w zależności od tego, ile innych rzeczy może wykonać twój system. Czy są to przeciętne czasy około 10 lub 20 przebiegów, czy tylko czasy od jednego przebiegu? – JGroven

+1

Prawdopodobnie 512 to magiczny rozmiar, który strasznie pasuje do pamięci podręcznej, więc dostajesz dużo braków w pamięci podręcznej. Inne rozmiary pasują lepiej, więc dostajesz mniej braków. – NathanOliver

+0

Zła droga @NathanOliver - 512 to dużo * wolniej * niż 513. –

Odpowiedz

6

Jest to spowodowane brakami w pamięci podręcznej. Możesz użyć numeru valgrind --tool=cachegrind, aby zobaczyć liczbę pomyłek. Korzystanie N = 512 masz następujący wynik:

Average for size 512: 13052 (us)==21803== 
==21803== I refs:  1,054,721,935 
==21803== I1 misses:   1,640 
==21803== LLi misses:   1,550 
==21803== I1 miss rate:   0.00% 
==21803== LLi miss rate:   0.00% 
==21803== 
==21803== D refs:  524,278,606 (262,185,156 rd + 262,093,450 wr) 
==21803== D1 misses:  139,388,226 (139,369,492 rd +  18,734 wr) 
==21803== LLd misses:   25,828 (  7,959 rd +  17,869 wr) 
==21803== D1 miss rate:   26.6% (  53.2%  +   0.0% ) 
==21803== LLd miss rate:   0.0% (  0.0%  +   0.0% ) 
==21803== 
==21803== LL refs:   139,389,866 (139,371,132 rd +  18,734 wr) 
==21803== LL misses:   27,378 (  9,509 rd +  17,869 wr) 
==21803== LL miss rate:   0.0% (  0.0%  +   0.0% ) 

czas, korzystając N=530 co masz następujący wynik:

Average for size 530: 13264 (us)==22783== 
==22783== I refs:  1,129,929,859 
==22783== I1 misses:   1,640 
==22783== LLi misses:   1,550 
==22783== I1 miss rate:   0.00% 
==22783== LLi miss rate:   0.00% 
==22783== 
==22783== D refs:  561,773,362 (280,923,156 rd + 280,850,206 wr) 
==22783== D1 misses:  32,899,398 (32,879,492 rd +  19,906 wr) 
==22783== LLd misses:   26,999 (  7,958 rd +  19,041 wr) 
==22783== D1 miss rate:   5.9% (  11.7%  +   0.0% ) 
==22783== LLd miss rate:   0.0% (  0.0%  +   0.0% ) 
==22783== 
==22783== LL refs:   32,901,038 (32,881,132 rd +  19,906 wr) 
==22783== LL misses:   28,549 (  9,508 rd +  19,041 wr) 
==22783== LL miss rate:   0.0% (  0.0%  +   0.0% ) 

Jak widać, D1 strzela w 512 jest około 3,5 razy większa niż 530

+0

Tak więc jeden rozwiązaniem byłoby użycie następnej matrycy o większym rozmiarze dla matrycy "przyjaznej dla pamięci podręcznej", pozostawiając kilka nieużywanych kolumn (i ewentualnie wierszy), ale powinno być ono szybsze. – rcgldr

+0

Yup, a wysoki odsetek błędów polega na tym, że asocjatywność pozwala na użycie tylko części całkowitej pamięci podręcznej, pod pewnymi wzorami dostępu do pamięci. –

+1

@rcgldr: Lepszym rozwiązaniem byłoby zmienić kolejność dostępu do pamięci. Zamiast zamieniać pojedyncze elementy, zamień blok 4x4, w ten sposób uzyskasz dostęp do wszystkich elementów w tym samym wierszu pamięci podręcznej dla obu końców zamiany. –

Powiązane problemy