主頁 >  其他 > 撰寫快取友好型程式技巧

撰寫快取友好型程式技巧

2022-03-21 07:55:49 其他

通過使用資料快取加速程式

譯者注:本文原始鏈接為<Make your programs run faster by better using the data cache>,翻譯獲得作者同意,本文中的一些策略只對大量資料處理有優化的可能,小量資料很可能帶來性能下降,

通過使用資料快取加速程式

開發者時刻面臨著如何加速程式,其中最明顯的是通過花哨的演算法來降低復雜度,比如說將\(O(n^2)\) 復雜度的演算法,使用 \(O(nlogn)\) 替換等等,這是很好的方法,但是并不是所有的程式都有更好的演算法來優化,那么應該怎么辦?是否存在一種方法來優化我們已存在的演算法,事實上是存在的,它叫:底層優化(low-level optimizations),

首先,先來了解一下底層優化的情況,底層優化關注于如何最好地利用底層架構的特殊性來獲得更好的性能,這是這個系列的第一篇,將會涉及到底層優化,我們將在如何更好地利用記憶體快取子系統上探索一系列的方法,對于熟悉現代多行程系統記憶體快取原理的讀者,可以調過 資料快取 章節,

資料快取

計算機系統通常包含處理器和記憶體,在現代計算機系是中記憶體是比處理器慢百倍的,因此處理器通常都要等待記憶體傳輸資料,聰明的硬體工程師想出了一個解決方案來抵消速度上差異:他們添加了一個很小的快速記憶體被稱為快取的東西,用以彌補不同的速度,當程式需要訪問主存中的資料,它首先會檢查資料是否已經在快取中,如果在,它可以很快的獲取資料,否則,計算機會犧牲一些處理器周期,等待主記憶體提供資料,

通常情況下,快取存盤器分為指令快取存盤器和資料快取存盤器,指令快取器的目的是加速對指令的訪問,資料緩沖器的目的是加快對指令所使用的資料的訪問,這篇文章主要關注如何使用資料快取器來加速程式,

為什么記憶體快取可以使程式運行的更快?

那么,為什么增加快取記憶體能起作用呢? 畢竟,程式可以在任何時候都可以訪問任何記憶體位置,因此,這些資料不應該在快取中,理論上是這樣的,但在實踐中,真正的程式幾乎不會以隨機方式訪問記憶體,

有兩個原則制約著現實世界的程式行為,第一條被稱為時間區域性,即如果處理器目前正在訪問某個記憶體地址,就很有可能在不久的將來訪問這個記憶體地址(例如:回圈中的計數器),第二種被稱為空間區域性,其含義是如果處理器目前正在訪問某個記憶體地址,那么它很有可能會在不久的將來訪問鄰近的記憶體地址(例如:處理陣列中的資料),

資料快取內部組織形式

現在讓我們來看看快取的內部情況,在現代計算機處理器中被分解為 Cache line,每個 Cache line 通常情況下是 64bytes 的大小,一個 Cache line 對應主存中 64byte 的記憶體塊,獲取 64byte 中的任意 1byte 資料,意味著需要加載整個 64byte 記憶體塊到快取中來,當 cache line 長時間沒有使用或需要使用新的資料去替換,快取將回傳修改后的塊到主存中去,這整個程序程式并不知道,

讓程式運行更快的一些技巧

對資料快取有了一定的了解后,讓我們來看看如何將資料快取相關的原理應用到你的程式中,從而使你的程式擁有更好的性能,

注意:下面每小節,我使用 C 寫法的陣列和 C++ 寫法的陣列(vector、array),以及 C 寫法(struct)和 C++ 寫法(class)的類,

當獲取線性資料時,盡量使用vector和array

鏈表,hash maps,字典等,都是十分有用的資料結構,但是,它們都不是快取優化的,在這些資料結構中遍歷,將會帶來很大的快取丟失,如果,當前程式性能是最重要的因素,那么將這些資料結構轉為陣列, 如果這是不可能的,嘗試將陣列的快取效率和其他資料的靈活性結合起來,Gap_buffer 是這種結合的很好例子,它將鏈表結構和陣列結構結合,這使得其具有卓越的快取效率和較低代價的元素添加和洗掉,另一個例子是 Judy_array ,稀疏陣列的樹形實作,同樣的擁有很低代價的元素插入和洗掉并且快取友好,

經常訪問的資料,在記憶體中應當是相鄰的

如果有幾個變數被一起訪問,它們應該被逐一宣告,這就增加了另一個變數在處理器訪問后已經在快取中的可能性,因為在處理器訪問了第一個變數之后,另一個變數已經在快取中了,從而避免了緩沖區的缺失,

考慮下面的類:

 class free_memory_list {
    void* head; /// Pointer to the beginning of the list
    Statistics statistics; /// Statistics about list usage
    int count; /// Number of elements in the list
    Allocator* base_allocator; /// Pointer to the class used for memory
/// allocation and deallocation
};

這個類實作了一個鏈接指標串列,如果我們的程式以這樣的方式使用該類,即把變數 head 和 count 作為一個包來訪問,那么它們應該一個接一個地放在類定義中,在這種情況下我們增加了它們實際在同一快取行中的概率,

使用資料陣列替換指標陣列

當談到類或結構的陣列時,人們想到的第一個想法是使用指標而不是值,這種解決方案與陣列相比有很多優點,包括運行時的多型性和在陣列中未分配元素的情況下,對記憶體的使用更少,但會有一個性能缺陷, 使用指標訪問該變數時,必然會涉及到高速快取的缺失, 因此,為了快速訪問陣列,可以不使用指標而使用值,

因此,現在當把類的陣列作為值時,我們將獲得一些好處,每當我們訪問陣列中的一個元素時,快取預取器就會獲取更多與我們當前訪問的元素接近的元素, 如果,我們訪問的是陣列中彼此相鄰的元素,資料快取被最大限度地利用了,

優化對類或結構陣列的訪問

如果我們以隨機的方式訪問陣列中的元素,就會出現一些快取丟失的情況,但是我們會有更多或者更少的丟失取決于我們在類中如何組織資料,例如:

假設我們有一個類my_class 并且該類的大小為 48btyes,陣列的第一個元素從偏移量 0 開始,第二個元素從偏移量 48 開始,第三個元素從偏移量 96 開始,第四個元素從偏移量 96 開始,如果我們的cache line是64bytes,那么意味著第一個類元素在第一個 cache line 中,第二個元素將被分別存在第一個和第二個cache line中,第三個元素也將被分別存在兩個 cache line 中...

在對類的元素進行隨機訪問的情況下,將一個元素分割在兩個高速快取線之間,從資料快取利用率的角度來說是不好的,快取存盤器需要兩次訪問主存盤器才能讀取一個元素,那么如何避免呢?如何使每個元素適合最小數量的cache line?下面是一些規則:

  • 類的大小需要是cache line大小的倍數,
  • 陣列的起始地址需要是cache line大小的倍數,

要使類的大小是快取行大小的倍數,我們可以手動的填充使其滿足cache line 或者告訴編譯器自動幫我們填充,在C++11中允許使用alignas(64) 來指定,當然 GCC/CLANG 編譯器提供了 __attribute__((aligned (64))) 實作相同功能,更好的解決方案是讓編譯器和庫來幫助我們;我們可以用 posix_memalign 去分配對齊的記憶體在堆中,或者使用alignas(64)__attribute__((aligned (64))) 在堆疊或者全域記憶體中分配記憶體,下面是一個使用posix_memalign分配類陣列的例子:

my_class* array_of_my_class;
posix_memalign((void**)array_of_my_class, 64, SIZE * sizeof(my_class));
for (size_t i = 0; i < SIZE; i++) {
 ::new (&array_of_my_class[i]) my_class(i);
}

語法看起來非常的復雜,其實很簡單,我們宣告了my_class 型別的指標,該指標用來保存類陣列,接下來我們使用posix_memalign 去為陣列分配記憶體,同時指定了對其引數為64,最后,在回圈中,我們為陣列中的每個元素呼叫建構式,注意,我們使用的是::new運算子,這個運算子不做記憶體分配,而是它在作為引數提供的記憶體上執行建構式(簡單的說,就是執行 array_of_my_class[i] 建構式,而不進行記憶體分配),

有效地訪問你的矩陣中的資料

如果你的程式需要處理矩陣,你需要了解矩陣是如何存盤在記憶體中的,矩陣被定義為二維的,但是記憶體是一維的,C 和 C+ 編譯器將矩陣逐行排列,這就意味著如果我們獲取矩陣中的某個元素,和這個元素在相同行的一些元素也已經被加載到資料快取中,

這看起來微不足道,但它會對性能產生深遠的影響,考慮一下簡單的矩陣乘法演算法:

void multiply_matrices(int in_matrix1[][N], int in_matrix[][N], int result[][N])
{
    int i, j, k;
    for (i = 0; i < N; i++) {
        for (j = 0; j < N; j++) {
          result[i][j] = 0;
             for (k = 0; k < N; k++) {
             result[i][j] += in_matrix1[i][k] * in_matrix[k][j];
             }
         }
    }
}

矩陣相乘就是對in_matrix1 的行,和對in_matrix2 的列做運算,按照這個計算規律結合上文的內容,我們可知in_matrix1 是符合快取規律的,但是,in_matrix2 將會是緩沖的災難,在in_matrix2 中每訪問下一個元素都會導致 cache 丟失,因此上面的代碼具有很大的性能缺陷,

那么如何解決上述問題?其實很簡單,進行一種叫做回圈互換的轉換, 將j上的回圈移到最里面的位置,這個解決方案的具體實作如下:

void multiply_matrices(int in_matrix1[][N], int in_matrix[][N], int result[][N]) 
{ 
    int i, j, k; 
    for (i = 0; i < N; i++) { 
        for (j = 0; j < N; j++) { 
            result[i][j] = 0; 
        }
        for (k = 0; k < N; k++) {
            for (j = 0; j < N; j++) {
                result[i][j] += in_matrix1[i][k] * in_matrix[k][j]; 
        } 
    } 
}

通過這種轉換,逐行運行in_matrix,也要逐行運行result,這對性能有很大的影響,

在你的類和結構體中避免填充

關于資料對齊的一個小說明, 適用于所有原始資料型別,如果資料型別為4位元組大小,其起始地址需要能被 4 整除,如果資料型別為8位元組大小,其起始地址需要能被 8 整除,以此類推對于N位元組大小的資料型別,它的起始地址應當能被 N 整除,我們說變數是正確對齊的,或是對齊的,此外都是未對齊,對齊要求通常是由硬體提出的,并由編譯器盡可能地強制執行,

為了確保你的類和結構中的資料能正確對齊,C 和 C++ 編譯器可以添加填充:這些是在類的成員之間添加的未使用的位元組,以確保所有成員都正確對齊,請看下面的例子:

class my_class {
  int my_int;
  double my_double;
  int my_second_int;
};

人們期望這個結構體的大小是sizeof(int) + sizeof(double) + sizeof(int) = 16 ,然而,double需要8位元組對其,因此在my_int 之后,編譯器添加了 4bytes 的填充,此時my_double 記憶體對齊了,因此,當前的結構體需要20bytes,

此外,為了使類的資料在陣列中正確對齊,類采取了根據其最大成員大小來對齊,而且類的大小是其最大成員大小的整數倍,在上面的例子中,整型是 4 位元組對齊,雙精度浮點型是 8 位元組對齊,因此我們的類應當 8 位元組對齊,由于類的大小需要是其對齊方式的倍數,編譯器在類的末尾增加了 4 位元組的填充,因此,類的大小從原來的 20 位元組增加到 24 位元組,填充物是如何影響快取效率的?比方說,我們的高速快取存盤器有一個 64 位元組的 cache line,在我們的例子中,只有2.7個my_class 類可以填充到 cache line 中,而不是我們所期望的四個類實體,另外,填充位元組被加載到高速快取中,但是你的程式是不會用這些填充位元組的,這保證了編譯器不會插入任何填充物,程式將更好地使用資料快取,下面對上面類進行了修改的類,其類成員的順序略有修改,從而避免編譯器填充,

class my_class {
  double my_double;
  int my_int;
  int my_second_int;
};

這個類的大小現在是 16 位元組,四個類實體符合一個 cache line 大小,

在linux中有個叫做 pahole工具可以查找你類的填充,它需要從原始碼構建,并且不支持 C++11,

盡可能的使用最小的型別

盡可能的使用位元組數少的型別也是避免編譯器填充的一種方法,有時short 可以滿足需求的,我們卻習慣性的用int ,或者說在你的類的定義中有幾個bool變數,你用它來保持各種flag,你可以用charsbool位域來代替使用bool

class my_class {
public:
    bool my_bool1:1;
    bool my_bool2:1;
    bool my_bool3:1;
    bool my_bool4:1;
    int my_int:1;
};

在上面的例子中,每個 bool 占了 1 bit,但是,編譯器為了和 my_int 對齊會在 my_bool4 后面自動填充,這可以通過重新安排my_class成員的順序來避免,

另外的例子:一個64位元組的cache line可以存盤8個整型或者4個長整型, 如果你有一個 1M 元素的陣列,若該陣列元素是整型那么將占 4MB,若為長整型就是 8MB,顯而易見的是,加載 8MB 到緩沖中,比加載 4MB 慢,

但要注意!處理器在內部以原生位元組數(例如:8位元組對于現代64位計算機,或者在嵌入式系統的結構中,可能適用更小的位元組數)為最佳作業方式, 使用較小的資料型別甚至可能降低速度,因此,你將需要測量以獲得確定的性能差異,

盡可能避免堆分配

由于下面幾個原因,堆的效率很低:

  • 呼叫 mallocfree 是很慢的,
  • 對這些位置的訪問是間接(通過指標)的,這種方式將導致更多的高速快取不命中的情況,

另一方面,堆疊頂幾乎總是在快取中,分配和洗掉的速度超快,你可以用下面幾個技巧來加速,如果,你的程式需要分配可變大小的陣列,可以考慮在堆疊而不是堆上分配陣列( GCC 支持但是有些編譯器不支持),如果,你的程式需要使用malloc分配大量的小記憶體塊,可以考慮分配一個大的記憶體塊,然后根據你的需要把它分成小的,如果,你的程式需要頻繁的分配和釋放同一型別的物件,那么我們可以考慮快取記憶體塊,而不是直接釋放,如果我們重新需要時,可以很快的重新分配,

盡可能的重復使用緩沖中的資料

理想情況下,我們希望從記憶體中加載資料到快取中,對其進行一些修改,然后再把它們回傳到操作記憶體中,如果你需要兩次獲取相同的資料,你就沒有最佳地使用快取,

以查找一個陣列中的最大元素和最小元素為例,

int * a = initialize_array(size);
int min = find_min(a, size);
int max = find_max(a, size);

我們在這里呼叫了兩個函式,一個是找到最大值,一個是找到陣列中的最小值,每個函式都有自己的回圈,并在回圈中遍歷陣列中的元素,

假設陣列足夠大,陣列中的元素將被加載到快取中兩次,

解決辦法很簡單:在一個回圈中完成對陣列的所有作業,下面是更正后的版本,

int * a = initialize_array(size);
int min = a[0];
int max = a[0];
for (int i = 0; i < size; i++) {
  min = std::min(a[i], min);
  max = std::max(a[i], max);
}

這里我們只對陣列進行一次迭代,陣列資料只被加載到資料快取中一次,這樣可以更好地利用資料快取,

盡量避免寫記憶體

所有對記憶體的寫入都要經過資料高速快取,當進行寫操作時,快取將該 cache line 標記為 "dirty",若一個 cache line 是 dirty 的,這就意味著他和記憶體中的資料不同,它的內容遲早會被寫回記憶體中,這導致了速度的減慢,請看這兩個演算法:

void sort_fast(int* a, int len) {
    for (int i = 0; i < len; i++) {
        int min = a[i];
        int min_index = i;
        for (int j = i+1; j < len; j++) {
            if (a[j] < min) {
                min = a[j];
                min_index = j;
            }
        }
        std::swap(a[i], a[min_index]);
    }
}
void sort_slow(int* a, int len) {
    for (int i = 0; i < len; i++) {
        for (int j = i+1; j < len; j++) {
            if (a[j] < a[i]) {
                std::swap(a[j], a[i]);
            }
        }
    }
}

上面的代碼顯示了兩個類似的數字排序的函式,兩者的作業方式相同,找到最小的元素并把它放在 0 的位置,然后找到下一個最小的元素并把它放在 1 的位置,以此類推,

方法 sort_slow 尋找比 a[i] 小的陣列元素,如果找到就立即交換, 每當發現一個比 a[i] 小的元素時,它就會繼續交換元素,方法sort_fast 尋找陣列中比a[i]小的元素,但它不做交換,而是將新的最小元素保存在臨時變數min和min_index中(編譯器為這些臨時變數使用一個暫存器), 當函式完成對陣列的運行并找到最終最小的元素時,才會替換a[i]和a[min_index]的內容, 在我的系統上,函式sort_fastsort_slow快 2 倍,

正確對齊你的資料

你的變數需要正確對齊,這樣可以確保整個變數位于一個快取行中,而不是被分割在兩個快取行中,如果你的系統不支持對錯位變數的訪問,獲取資料會變得非常慢,因為你的作業系統可能會模擬對錯位記憶體地址的訪問,大多數時候,編譯器會確保資料正確對齊,但不細心開發者可能會創造出記憶體訪問錯位的地方,這種情況經常發生在將指標從一種型別轉換為另一種型別指標的時侯,錯誤示范:

unsigned char serialized_data[1024];
read_data(serialized_data);
int* header_pointer = (int*) (serialized_data + 3);
int header = *header_pointer; 

在這個例子中,我們將 char 指標轉換為 int 指標,從而使 header_pointer 錯位,解除對header_pointer的參考會產生錯位的記憶體訪問,在某些架構上,程式會崩潰,在其他架構上,程式會變慢,正確示例:

unsigned char serialized_data[1024];
read_data(serialized_data);
int* header_pointer = (int*) (serialized_data + 3);
int header;
memcpy(&header, header_pointer, sizeof(int));

在這里,我們使用memcpy將輸入陣列中的值復制到 header 變數中,函式memcpy不需要適當的對齊,這段代碼更好地利用了資料快取,而且它是可移植的,

使用軟體預取

如果你的演算法不是一個一個地訪問資料,而是以隨機的方式在記憶體中跳躍,你可以使用軟體預取來告訴處理器你將訪問哪些資料,這樣它就有時間在需要它們之前將它們加載到快取中,例如,GCC 和 CLANG 編譯器提供了__builtin_prefetch內置,允許軟體預取,這里我們舉一個二分搜索演算法的例子:

int binarySearch(int *array, int number_of_elements, int key) {
    int low = 0, high = number_of_elements-1, mid;
    while(low <= high) {
        mid = (low + high)/2;
#ifdef DO_PREFETCH
        // low path
        __builtin_prefetch (&array[(mid + 1 + high)/2], 0, 1);
        // high path
        __builtin_prefetch (&array[(low + mid - 1)/2], 0, 1);
#endif
        if(array[mid] < key)
            low = mid + 1; 
        else if(array[mid] == key)
             return mid;
        else if(array[mid] > key)
             high = mid-1;
        }
}

二分搜索演算法不按順序運行陣列,硬體快取預置器面臨大量的快取缺失,在上面的二分搜索演算法中,我們在進行資料查找之前就預取了記憶體中 high 的新值和 low 的新值,而當我們真正需要這些資料時,它已經存在于快取中了,

這個特定演算法的缺點是,我們獲取兩個值,而其中一個值我們將永遠不會被使用,這可能會對性能產生影響,因為一些 cache line 現在需要離開快取,以便為未使用的值騰出位置,如果另一個程式在另一個使用相同快取的核心上運行,可能會導致兩個程式的性能下降,

實驗

讓我們看看這些技巧對我們的實際問題有何幫助,在測驗中,我們使用一個普通的通用系統AMD A8-4500M CPU,有四個核心,每個核心有16KB的L1資料快取,每個核心有2MB的L2資料快取(兩個核心共享2MB的L2資料快取),這個系統有12GB的記憶體,

矩陣乘法

這里有一篇由 Ulrich Drepper 寫的關于快取優化的文章,非常好,但也很冗長,他從簡單的矩陣乘法演算法開始,不斷完善,直到得到一個優化的版本,速度幾乎快了十倍,在他的實驗中,他使用了一個1000×1000的雙精度浮點矩陣, 他做的第一個優化是在乘法前對矩陣進行轉置,僅此一項就帶來了76.6%的性能提升!

帶軟體預取的二分搜索演算法

我們實作了一個使用前一章中的軟體預取的二進制搜索演算法,我們用它來測驗當程式員明確使用預取時,記憶體快取子系統是如何作業的,我們使用的程式原始碼放在了 github 中,只需要輸入make binary_search_runtimesmake binary_search_cache_misses 就可分別生成帶預取和不帶預取的程式,

我們生成了一個排序的隨機整數陣列,長度為 10K, 100K, 1M, 10M 和 100M,我們用這些陣列來進行二分搜索,我們稱其為 input_array ,我們使用幾種不同的方法來生成 index_array 的索引,并使用隨機的方法來生成 index_array 的元素, 但我們也使用了一種基于非隨機步長的方法, 如果步長是 N,index_array 的第一個元素是 0,第二是 N,第三是 2N 等等,直到我們到達陣列的末端,然后繞過,

因此,代碼實作如下:

 for (int i = 0; i < len; i++) {
    binarySearch(inputArray, len, inputArray[indexArray[i]]);
 }

我們對隨機 index_array 和步長為 1、100 和 10K 的 index_array 都進行了測驗, 我們將 index_array 的大小固定為 10M ,這意味著我們正在進行 10M 的搜索, 對于隨機訪問的 index_array,下面是結果,

作業集 關閉Prefetching 開啟Prefetching 速度差異
10K 1673ms 1777ms -6.2%
100K 2478ms 2426ms +2.1%
1M 4519ms 3996ms +11.6%
10M 8804ms 7096ms +19.4%
100M 14970ms 11685ms +21.9%

上表表示了在隨機訪問的情況下二分搜索的運行時間和作業集大小的關系,

對于隨機訪問,軟體預取對最小的10K資料集來說是負優化,隨著作業集的增加,帶軟預取的演算法也會變得比普通演算法快,對于大的作業集來說,速度上的差異大約是20%,而且即使作業集的大小進一步增加,它也會保持這種狀態,如果我們不是在訪問隨機元素,而是在以恒定的步長訪問元素,會發生什么?由于這是一個合成測驗,我們正在尋找一個我們事先知道位置的元素,例如:如果步長是 100,我們知道第一個元素的位置是 0,我們正在尋找 input_array[0] 這個值,在下一次迭代中,我們要尋找一個元素 input_array[100] 等等, 在這種情況下,快取是如何表現的?下面是結果,

作業集 關閉Prefetching 開啟Prefetching 速度差異
10K 977ms 1168ms -19.5%
100K 1122ms 1380ms -23.0%
1M 1367ms 1623ms -18.7%
10M 1610ms 1892ms -17.5%
100M 1813ms 2171ms -19.7%

上表是 步長=1 的情況下二分搜索的運行時間和作業集大小的關系,

作業集 關閉Prefetching 開啟Prefetching 速度差異
10K 1112ms 1418ms -27.5%
100K 1766ms 1942ms -9.7%
1M 3018ms 2792ms +7.5%
10M 3236ms 3019ms +6.7%
100M 3356ms 3182ms +5.2%

上表是 步長=100 的情況下二分搜索的運行時間和作業集大小的關系,

作業集 關閉Prefetching 開啟Prefetching 速度差異
10K 760ms 984ms -29.5%
100K 1049ms 1508ms -43.8%
1M 2739ms 2640ms +3.6%
10M 4402ms 7490ms -70.1%
100M 10395ms 8251ms -20.6%

上表是 步長=10K 的情況下二分搜索的運行時間和作業集大小的關系,

對于stride=1,普通演算法每次都能擊敗軟體預取演算法,不過這并不奇怪,因為前一次迭代的所有資料都已經在快取中了,

對于stride=100,正如預期的那樣,對于小的作業集,一般的演算法勝過軟體預取演算法,但是隨著作業集的增加,軟體預取演算法平均快6%,為什么在這種情況下,與完全隨機的情況下的20%相比,平均快6%?如果我們的作業集是10M,平均來說,該演算法在20步左右完成,在這20步中,有14步的資料已經在之前搜索的快取中了,因此,只有在最后的6步中,軟體預取才有意義,

對于stride = 10K,我們看到一個非常奇怪的行為,即一般演算法比軟體預取演算法快,為什么呢?答案是,在步長為10K時對于輸入的10M的作業集,這相當于將其減少到只有10K的作業集,因此,即使是最大的作業集大小(10M),通用演算法也更快,與隨機索引和作業集10K的通用演算法的百分比幾乎相同,

快取性能如何?預取對快取性能有什么樣的影響,讓我們比較一下有預取和無預取的pref指令的結果,

Parameter 關閉Prefetching 開啟Prefetching 差異
快取參考 409M 649M +58.7%
快取丟失 155M 254M +63.9%
Cycles 31481M 25648M -18.5%
指令數 6659M 10647M +59.9%

兩個版本的二分搜索的快取性能、周期和指令資料,結果是有趣的,與普通版本相比,軟體預取版本有更多的快取參考,更多的快取丟失和更多的指令執行,這意味著,軟體預取版本實際上做了更多的作業,但它還是更快,因為它在較少的周期內做了更多的作業,現代處理器每個周期可以執行一條以上的指令,但如果處理器需要等待從主存盤器中獲取資料,這個數字就會下降,如果我們計算每個周期的指令數,軟體預取版本為0.42,普通版本為0.21,這是一個巨大的差異,

快取友好的鏈表

第二個實驗是一個有利于快取的鏈表(這些串列也被稱為 unrolled 鏈表),普通的鏈表對快取非常不友好,在這樣的結構中迭代會導致很多快取丟失,我們怎樣才能做得更好呢?

常規的鏈接串列通常由鏈接串列節點組成,每個節點持有一個值和指向下一個節點的指標,在我們的實作中,鏈表節點可以持有一個以上的值,值的數量被指定為 linked_list 類的一個模板引數,我們測驗了可以容納 1、2、4和 8 個值的鏈接串列節點,節點中值的數量越大,快取失誤就越少,我們用來測驗的程式的源代碼可以在 github 上找到,要運行它,只需執行 make linked_list_runtimes 和 make linked_list_cache_misses 即可,

我們唯一感興趣的是測驗在鏈表中的進行插入、洗掉等,我們的實作也比簡單的鏈表快,但大部分的性能改進來自于更少的記憶體分配,而不是更好地使用資料快取,下面是一個單一串列節點的源代碼,

    class linked_list_node {
    public:
        char used_elems[count];
        linked_list_node* next;
        char values[count * sizeof(Type)];
        ...
};

常量 count 保存單個串列節點中的值的數量,模板常量 Type 是我們在節點中保存的型別,由于一個節點有不止一個值,我們在陣列 used_elems 中標記哪些值是實際使用的,注意,我們使用字符來保存使用的值,而不是bool,在我的機器上,bool需要四個位元組,而char需要一個位元組,這個選擇確實提高了串列的性能,因為更多來自節點的其他資料將被預取到快取中,

現在讓我們開始測量,下面的圖表測量了迭代鏈表所需的時間,這取決于節點中的值的數量(1、2、4和8)和鏈表的值的型別大小,

image

結果看起來非常好! 對于最小的型別大小,遍歷一個節點中有兩個值的鏈表比遍歷一個值的鏈表要少花43%的時間,而且這個數字對于所有型別大小都大致相同,還要注意的是,串列中的型別大小越大,在單個鏈表節點中擁有更多的值就越有意義,對于4位元組的型別大小,有兩個值的節點和有四個值的節點之間的差異很小,但對于32位元組的型別大小,這種差異是明顯的,

快取性能如何?在我們的測驗中,記憶體讀取次數和資料快取失誤率是多少?首先讓我們測量一下我們的程式有多少次記憶體讀取,

image

正如你所看到的,單個節點中存盤的數值越多,讀取的記憶體總量就越少,這是可以預期的,因為涉及的指標算術較少,請注意,在我們的例子中,記憶體讀取的數量并不取決于型別大小,

快取缺失情況如何?我們使用了 cachegrind 工具來測量高速快取失誤率,它可以提供關于每個函式或每行的快取缺失的資訊,下面是結果,

image

一般的趨勢是:如果一個鏈接串列中的值越多,快取缺失的次數就越少,對于最小的型別,節點中8個值的快取丟失率為3%,而節點中1個值的快取丟失率為10%,對于較大的型別也觀察到類似的比率,另一個趨勢是:在一個鏈接串列節點中,型別大小越大,快取缺失率就越大,對于4位元組的型別大小,最壞的情況下快取丟失率為10%,最好的情況下為3%,對于32位元組的型別大小,最壞的情況下快取缺失率為20%,最好的情況下為13.6%,沒有什么意外,當我們訪問節點中的第一個值時,快取預置器會在周圍的記憶體地址中加載值,所以當我們以后需要它們時,它們已經在快取中了,如果型別越大,預置器加載有用資料的機會就越少,所以這也是造成額外的緩沖區失誤的原因,

總結

那么結論是什么呢?無論是在我們的實驗中還是在一般情況下,關于資料快取的優化肯定有一些有趣的說法,

關于實驗的總結

那么結論是什么呢?我們先談談我們的實驗:這三個實驗都表明,更好地利用資料快取,會取得性能上的提升,與非優化版本相比,優化后的矩陣乘法有很好的性能提升,但是除了矩陣之外,我們很少能在資料結構上做一個簡單的操作來使我們的結構更適合快取,而在我的專業經驗中,我也很少看到矩陣作為資料結構使用,

第二個實驗是關于軟體預取的,我們確實看到了軟體預取在二分搜索上的性能提升,但性能提升只與不合適資料快取的作業集有關,需要注意,軟體預取是有代價的:我們的系統執行了更多的指令,并將更多的資料加載到快取中,這在某些情況下可能會導致其他方面的性能下降,軟體預取的好處是,它很容易實作,可以用來加快各種結構的迭代速度:鏈表、樹、堆等,然而,需要仔細測量以確保性能的提高是值得的,

我們的第三個實驗是關于快取友好的鏈表,我們再次看到速度有了很大的提高,改進來自幾個方面:更少的記憶體分配,更小的資料緩沖區失誤率和更少的指標運算指令,唯一的缺點是實作更復雜,而且記憶體消耗增加,因為可能會出現單個節點沒有被最佳填充的情況,但一般來說,我認為這些缺點是值得努力的,

對于資料快取優化的總結

正如我們的實驗所顯示的,以及我之前的經驗,優化確實帶來了性能的提升,而且有許多應用都需要這樣做,性能的提高取決于許多因素,范圍可以從百分之幾到百分之百甚至更多,

然而,這里提出的一些建議會使你的程式更難理解,更難除錯,而開發人員一直認為要避免的正是這一點,原因很充分,為了最大限度地從這些優化中獲益,你需要仔細地對你的程式進行剖析,找到瓶頸,并專注于加速這些瓶頸,而不是在你的代碼中到處應用這些技巧,首先,應當保持你的介面干凈,避免模塊間的依賴,然后用更快的代碼替換你的慢代碼就很簡單了,你可以在不犧牲可維護性的情況下享受性能的提升,

參考

Ulrich Drepper “What every programmer should know about memory”

What is cache memory – Gary explains

Agner.org – Software Optimization Resources

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/445578.html

標籤:其他

上一篇:鏈表和陣列的區別

下一篇:【面經】Java崗位常見面試題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more