簡體   English   中英

如何在C和java中生成cpu緩存效果?

[英]How to produce the cpu cache effect in C and java?

在Ulrich Drepper的論文中,每個程序員應該了解內存 ,第三部分:CPU緩存,他顯示了一個圖表,顯示了“工作集”大小與每個操作消耗的cpu周期之間的關系(在這種情況下,順序讀取)。 並且圖中有兩個跳轉,表示L1緩存和L2緩存的大小。 我編寫了自己的程序來重現c中的效果。 它只是簡單地從頭到尾順序讀取一個int []數組,我嘗試了不同大小的數組(從1KB到1MB)。 我將數據繪制成圖形並且沒有跳躍,圖形是直線。

我的問題是:

  1. 我的方法有問題嗎? 生成cpu緩存效果的正確方法是什么(查看跳轉)。
  2. 我在想,如果它是順序讀取,那么它應該像這樣操作:當讀取第一個元素時,它是緩存未命中,並且在緩存行大小(64K)內,將有命中。 借助預取,將隱藏讀取下一個緩存行的延遲。 它會連續地將數據讀入L1緩存,即使工作集大小超過L1緩存大小,它也會驅逐最近最少使用的數據,並繼續預取。 因此,大多數緩存未命中都將被隱藏,從L2獲取數據所消耗的時間將隱藏在讀取活動之后,這意味着它們同時運行。 相關性(在我的情況下為8路)將隱藏從L2讀取數據的延遲。 那么,我的節目現象應該是對的,我錯過了什么嗎?
  3. 是否有可能在java中獲得相同的效果?

順便說一句,我在linux中這樣做。


編輯1

感謝Stephen C的建議,這里有一些額外的信息:這是我的代碼:

int *arrayInt;

void initInt(long len) {
    int i;
    arrayInt = (int *)malloc(len * sizeof(int));
    memset(arrayInt, 0, len * sizeof(int));
}

long sreadInt(long len) {   
    int sum = 0;
    struct timespec tsStart, tsEnd;

    initInt(len);

    clock_gettime(CLOCK_REALTIME, &tsStart);
    for(i = 0; i < len; i++) {
        sum += arrayInt[i];
    }
    clock_gettime(CLOCK_REALTIME, &tsEnd);
    free(arrayInt);
    return (tsEnd.tv_nsec - tsStart.tv_nsec) / len;
}

在main()函數中,我嘗試過從1KB到100MB的數組大小,仍然相同,每個元素的平均耗時為2納秒。 我認為時間是L1d的訪問時間。

我的緩存大小:

L1d == 32k

L2 == 256k

L3 == 6144k


編輯2

我已將代碼更改為使用鏈接列表。

// element type
struct l {
    struct l *n;
    long int pad[NPAD]; // the NPAD could be changed, in my case I set it to 1
};

struct l *array;
long globalSum;

// for init the array
void init(long len) {
    long i, j;

    struct l *ptr;

    array = (struct l*)malloc(sizeof(struct l));
    ptr = array;
    for(j = 0; j < NPAD; j++) {
        ptr->pad[j] = j;
    }
    ptr->n = NULL;

    for(i = 1; i < len; i++) {
        ptr->n = (struct l*)malloc(sizeof(struct l));
        ptr = ptr->n;
        for(j = 0; j < NPAD; j++) {
            ptr->pad[j] = i + j;
        }
        ptr->n = NULL;
    }

}

// for free the array when operation is done
void release() {
    struct l *ptr = array;
    struct l *tmp = NULL;
    while(ptr) {
        tmp = ptr;
        ptr = ptr->n;
        free(tmp);
    }
}

double sread(long len) {
    int i;
    long sum = 0;

    struct l *ptr;
    struct timespec tsStart, tsEnd;


    init(len);

    ptr = array;

    clock_gettime(CLOCK_REALTIME, &tsStart);
    while(ptr) {
        for(i = 0; i < NPAD; i++) {
            sum += ptr->pad[i];
        }
        ptr = ptr->n;
    }
    clock_gettime(CLOCK_REALTIME, &tsEnd);

    release();

    globalSum += sum;

    return (double)(tsEnd.tv_nsec - tsStart.tv_nsec) / (double)len;
}

最后,我將打印出globalSum以避免編譯器優化。 正如你所看到的,它仍然是一個順序讀取,我甚至嘗試了高達500MB的數組大小,每個元素的平均時間大約是4納秒(也許是因為它必須訪問數據'pad'和指針' n',兩次訪問),與1KB的數組大小相同。 所以,我認為這是因為像prefetch這樣的緩存優化很好地隱藏了延遲,我是對的嗎? 我將嘗試隨機訪問,並將結果放在以后。


編輯3

我試過隨機訪問鏈表,這是結果: 隨機訪問鏈表

第一個紅線是我的L1緩存大小,第二個是L2。 所以我們可以看到那里有點跳躍。 有時潛伏期仍然很好。

這個答案不是答案,而是一組筆記。

首先,CPU傾向於在高速緩存行上操作,而不是在單獨的字節/字/雙字上操作。 這意味着如果您按順序讀取/寫入整數數組,則第一次訪問緩存行可能會導致緩存未命中,但后續訪問同一緩存行中的不同整數則不會。 對於64字節高速緩存行和4字節整數,這意味着每16次訪問只能獲得一次高速緩存未命中; 這將稀釋結果。

其次,CPU有一個“硬件預取器”。 如果它檢測到正在按順序讀取高速緩存行,則硬件預取器將自動預取其預測接下來需要的高速緩存行(試圖在需要之前將它們提取到高速緩存中)。

第三,CPU執行其他操作(例如“亂序執行”)以隱藏獲取成本。 您可以測量的時間差(緩存命中和緩存未命中)是CPU無法隱藏的時間,而不是獲取的總成本。

這三件事合起來意味着; 為了順序讀取整數數組,當你從前一個緩存行執行16次讀取時,CPU可能會預取下一個緩存行; 任何緩存未命中成本都不會明顯,可能完全隱藏。 為了防止這種情況 您希望“隨機”訪問每個緩存行一次,以最大化“緩存中的工作集適合度”和“工作集不適合緩存/秒”之間測量的性能差異。

最后,還有其他因素可能影響測量。 例如,對於使用分頁的操作系統(例如Linux和幾乎所有其他現代操作系統),總共有一層緩存(TLB / Translation Look-aside Buffers),並且一旦工作集超過一定大小,TLB就會丟失; 這應該是圖中第四個“步驟”可見的。 還有來自內核的干擾(IRQ,頁面錯誤,任務切換,多個CPU等); 這可能是圖中隨機靜態/錯誤可見的(除非經常重復測試並丟棄異常值)。 還存在緩存設計(緩存關聯性)的工件,其可以以依賴於內核分配的物理地址/ es的方式降低緩存的有效性; 這可能被視為圖表中的“步驟”轉移到不同的地方。

我的方法有問題嗎?

可能,但沒有看到無法回答的實際代碼。

  • 您對代碼執行操作的描述並未說明您是在讀取數組一次還是多次。

  • 陣列可能不夠大......取決於您的硬件。 (難道一些現代芯片不具有幾兆的三級緩存嗎?)

  • 特別是在Java案例中,您必須以正確的方式執行許多事情來實現有意義的微基准測試。


在C案例中:

  • 您可以嘗試調整C編譯器的優化開關。

  • 由於您的代碼是以串行方式訪問數組,因此編譯器可能能夠對指令進行排序,以便CPU可以跟上,或者CPU可能會樂觀地預取或進行寬次提取。 您可以嘗試以較不可預測的順序讀取數組元素。

  • 編譯器甚至可能完全優化了循環,因為循環計算的結果不用於任何事情。

(根據這個Q&A - 從內存中獲取一個字需要多長時間? ,從L2緩存中獲取大約7納秒,從主內存中獲取大約100納秒。但是你得到的時間約為2納秒。聰明的東西必須在這里進行,以使其以您觀察的速度運行。)

使用gcc-4.7和使用gcc -std=c99 -O2 -S -D_GNU_SOURCE -fverbose-asm tcache.c進行編譯,您可以看到編譯器已經足夠優化以刪除for循環(因為未使用sum )。

我不得不改進你的源代碼; 一些#include -s丟失了, i沒有在第二個函數中聲明,所以你的例子甚至沒有按原樣編譯。

使sum成為一個全局變量,或者以某種方式將它傳遞給調用者(可能使用全局int globalsum;並在循環之后輸入globalsum=sum; )。

而且我不確定用memset清除數組是對的。 我可以想象一個聰明的編譯器理解你正在總結全零。

最后,您的代碼具有非常規則的行為和良好的局部性:偶爾會發生緩存未命中,整個緩存行被加載,數據足以進行多次迭代。 一些聰明的優化(例如-O3或更好)可能會生成良好的prefetch指令。 這對於高速緩存是最佳的,因為對於32字的L1高速緩存行,高速緩存未命中每32個循環發生,因此被很好地分攤。

制作鏈接的數據列表會使緩存行為變得更糟。 相反,在一些真正的程序中,在幾個精心挑選的地方仔細添加__builtin_prefetch可能會將性能提高10%以上(但添加太多會降低性能)。

在現實生活中,處理器花費大部分時間等待一些緩存(並且很難測量它;這等待是CPU時間,而不是空閑時間)。 請記住,在L3緩存未命中期間,從RAM模塊加載數據所需的時間是執行數百條機器指令所需的時間!

我不能肯定地說1和2,但在Java中成功運行這樣的測試會更具挑戰性。 特別是,我可能會擔心自動垃圾收集等托管語言功能可能會在測試過程中發生並導致結果丟失。

從圖3.26中可以看出,英特爾酷睿2在讀取時幾乎沒有任何跳躍(圖表頂部的紅線)。 正在寫入/復制跳轉清晰可見的地方。 最好做一個寫測試。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM