如何高效的訪問內存

如何高效的訪問內存

2011年11月23日lenky發表評論閱讀評論6,577 次瀏覽

影響內存訪問速度的因素主要有:
1.內存帶寬:每秒讀寫內存的數據量,由硬件配置決定。
2.CACHE高速緩衝:CPU與內存之間的緩衝器,當命中率比較高時能大大提供內存平均訪問速度。
3.TLB轉換旁視緩衝:系統虛擬地址向物理地址轉換的高速查表機制,轉換速度比普通轉換機制要快。

我們能夠優化的只有第2點和第3點。由於CACHE的小容量與SMP的同步競爭,如何最大限度的利用高速緩衝就是我們的明確優化突破口(以常用的數據結構體爲例):
1.壓縮結構體大小:針對CACHE的小容量。
2.對結構體進行對齊:針對內存地址讀寫特性與SMP上CACHE的同步競爭。
3.申請地址連續的內存空間:針對TLB的小容量和CACHE命中。
4.其它優化:綜合考慮多種因素

具體優化方法
1.壓縮結構體大小
系統CACHE是有限的,並且容量很小,充分壓縮結構體大小,使得CACHE能緩存更多的被訪問數據,無非是提高內存平均訪問速度的有效方法之一。
壓縮結構體大小除了需要我們對應用邏輯做好更合理的設計,儘量去除不必要的字段,還有一些額外針對結構體本身的壓縮方法。

1.1.對結構體字段進行合理的排列
由於結構體自身對齊的特性,具有同樣字段的結構體,不同的字段排列順序會產生不同大小的結構體。
大小:12字節

1
2
3
4
5
6
7
struct box_a
{
    char a;
    short b;
    int c; 
    char d;
}; 

大小:8字節

1
2
3
4
5
6
7
struct box_b
{
    char a;
    char d;
    short b;
    int c; 
};

1.2.利用位域
實際中,有些結構體字段並不需要那麼大的存儲空間,比如表示真假標記的flag字段只取兩個值之一,0或1,此時用1個bit位即可,如果使用int類型的單一字段就大大的浪費了空間。
示例:tcp.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct tcphdr {
    __be16  source;
    __be16  dest;
    __be32  seq;
    __be32  ack_seq;
#if defined(__LITTLE_ENDIAN_BITFIELD)
    __u16   res1:4,
        doff:4,
        fin:1,
        syn:1,
        rst:1,
        psh:1,
        ack:1,
        urg:1,
        ece:1,
        cwr:1;
#elif defined(__BIG_ENDIAN_BITFIELD)
    __u16   doff:4,
        res1:4,
        cwr:1,
        ece:1,
        urg:1,
        ack:1,
        psh:1,
        rst:1,
        syn:1,
        fin:1;
#else
#error  "Adjust your <asm/byteorder.h> defines"
#endif 
    __be16  window;
    __sum16 check;
    __be16  urg_ptr;
};

1.3.利用union
union結構體也是壓縮結構體大小的方法之一,它允許我們在某些情況下能對結構體的多個字段進行合併或把小字節字段存放到大字節字段內。
示例:skbuff.h

1
2
3
4
5
6
7
8
9
10
11
struct sk_buff {
    
    union {
        __wsum      csum;
        struct {
            __u16   csum_start;
            __u16   csum_offset;
        };
    };
    
};

2.對結構體進行對齊
對結構體進行對齊有兩層意思,一是指對較小結構體進行機器字對齊,二是指對較大結構體進行CACHE LINE對齊。

2.1.對較小結構體進行機器字對齊
我們知道,對於現代計算機硬件來說,內存只能通過特定的對齊地址(比如按照機器字)進行訪問。舉個例子來說,比如在64位的機器上,不管我們是要讀取第0個字節還是要讀取第1個字節,在硬件上傳輸的信號都是一樣的。因爲它都會把地址0到地址7,這8個字節全部讀到CPU,只是當我們是需要讀取第0個字節時,丟掉後面7個字節,當我們是需要讀取第1個字節,丟掉第1個和後面6個字節。
當我們要讀取的字節剛好落在兩個機器字內時,就出現兩次訪問內存的情況,同時通過一些邏輯計算才能得到最終的結果。
因此,爲了更好的提升性能,我們須儘量將結構體做到機器字(或倍數)對齊,而結構體中一些頻繁訪問的字段也儘量安排在機器字對齊的位置。
大小:12字節

1
2
3
4
5
6
7
8
struct box_c
{
    char a;
    char d;
    short b;
    int c; 
    int e; 
};

大小:16字節

1
2
3
4
5
6
7
8
9
struct box_d
{
    char a;
    char d;
    short b;
    int c; 
    int e; 
    char padding[4];
};

上面表格右邊的box_d結構體,通過增加一個填充字段padding將結構體大小增加到16字節,從而與機器字倍數對齊,這在我們申請連續的box_d結構體數組時,仍能保證數組內的每一個結構體都與機器字倍數對齊。
通過填充字段padding使得結構體大小與機器字倍數對齊是一種常見的做法,在Linux內核源碼裏隨處可見。

2.2.對較大結構體進行CACHE LINE對齊
我們知道,CACHE與內存交換的最小單位爲CACHE LINE,一個CACHE LINE大小以64字節爲例。當我們的結構體大小沒有與64字節對齊時,一個結構體可能就要佔用比原本需要更多的CACHE LINE。比如,把一個內存中沒有64字節長的結構體緩存到CACHE時,即使該結構體本身長度或許沒有還沒有64字節,但由於其前後搭佔在兩條CACHE LINE上,那麼對其進行淘汰時就會淘汰出去兩條CACHE LINE。
這還不是最嚴重的問題,非CACHE LINE對齊結構體在SMP機器上容易引發名爲錯誤共享的CACHE問題。比如,結構體T1和T2都沒做CACHE LINE對齊,如果它們(T1後半部和T2前半部)在SMP機器上合佔了同一條CACHE,如果CPU 0對結構體T1後半部做了修改則將導致CPU 1的CACHE LINE 1失效,同樣,如果CPU 1對結構體T2前半部做了修改則也將導致CPU 0的CACHE LINE 1失效。如果CPU 0和CPU 1反覆做相應的修改則導致的不良結果顯而易見。本來邏輯上沒有共享的結構體T1和T2,實際上卻共享了CACHE LINE 1,這就是所謂的錯誤共享。
Linux源碼裏提供了利用GCC的__attribute__擴展屬性定義的宏來做這種對齊處理,在文件/linux-2.6.xx/include/linux/cache.h內可以找到多個相類似的宏,比如:

1
#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES)))

該宏可以用來修飾結構體字段,作用是強制該字段地址與CACHE LINE映射起始地址對齊。
看/linux-2.6.xx/drivers/net/e100.c內結構體nic的實現,三個____cacheline_aligned修飾字段,表示強制這些字段與CACHE LINE映射起始地址對齊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct nic {
    /* Begin: frequently used values: keep adjacent for cache effect */
    u32 msg_enable              ____cacheline_aligned;
    /* 4字節空洞 */
    struct net_device *netdev;
    struct pci_dev *pdev;
    /* 40字節空洞 */
    struct rx *rxs              ____cacheline_aligned;
    struct rx *rx_to_use;
    struct rx *rx_to_clean;
    struct rfd blank_rfd;
    enum ru_state ru_running;
    /* 20字節空洞 */
    spinlock_t cb_lock          ____cacheline_aligned;
    spinlock_t cmd_lock;
    struct csr __iomem *csr;
    enum scb_cmd_lo cuc_cmd;
    unsigned int cbs_avail;
    struct napi_struct napi;
    
}

回到前面的問題,如果我們對結構體T2的第一個字段加上____cacheline_aligned修飾,則該錯誤共享即可解決。

2.3.只讀字段和讀寫字段隔離對齊
只讀字段和讀寫字段隔離對齊的目的就是爲了儘量保證那些只讀字段和讀寫字段分別集中在CACHE的不同CACHE LINE中。由於只讀字段幾乎不需要進行更新,因而能在CACHE中得以穩定的緩存,減少由於混合有讀寫字段導致的對應CACHE LINE的頻繁失效問題,以便提高效率;而讀寫字段相對集中在一起,這樣也能保證當程序讀寫結構體時,污染的CACHE LINE條數也就相對的較少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct {
    /* ro data */
    size_t block_count;     // number of total blocks
 
    size_t meta_block_size; // sizeof per skb meta block
    size_t data_block_size; // sizeof per skb data block
     
    u8 *meta_base_addr;     // base address of skb meta buffer
    u8 *data_base_addr;     // base address of skb data buffer
 
    /* rw data */
    size_t current_index    ____cacheline_aligned;  // index
     
} bc_buff, * bc_buff_t;

3.申請地址連續的內存空間
隨着地址空間由32位轉到64位,頁內存管理的目錄分級也越來越多,4級的目錄地址轉換也是一筆不小是開銷。硬件產商爲我們提供了TLB緩衝,加速虛擬地址到物理地址的換算。但是,畢竟TLB是有限,對地址連續的內存空間進行訪問時,TLB能得到更多的命中,同時CACHE高速緩衝命中的機率也更大。
兩段代碼,實現同一功能,但第一種方法在實際使用中,內存讀寫效率就會相對較好,特別是在申請的內存很大時(未考慮malloc異常):
方法一:

1
2
3
4
5
6
7
8
9
#define MAX 100
int i;
char *p;
struct box_d *box[MAX];
p = (char *)malloc(sizeof(struct box_d) * MAX);
for (i = 0; i < MAX; i ++)
{
    box[i] = (struct box_d *)(p + sizeof(struct box_d) * i);
}

方法二:

1
2
3
4
5
6
7
#define MAX 100
int i;
struct box_d *box[MAX];
for (i = 0; i < MAX; i ++)
{
    box[i] = (struct box_d *)malloc(sizeof(struct box_d));
}

另外,如果我們使用更大頁面(比如2M或1G)的分頁機制,同樣能夠提升性能;因爲相比於原本每頁4K大小的分頁機制,應用程序申請同樣大小的內存,大頁面分頁機制需要的頁面數目更少,從而佔用的TLB項目也更少,減少虛擬地址到物理地址的轉換次數的同時,提高TLB的命中率,縮短每次轉換所需要的時間。因爲大多數操作系統在分配內存時候都需要按頁對齊,所以大頁面分頁機制的缺點就是內存浪費相對比較嚴重。只有在物理內存足夠充足的情況下,大頁面分頁機制才能夠體現出優勢。

4.其它優化
4.1.預讀指令讀內存
提前預取內存中數據到CACHE內,提高CACHE的命中率,加速內存讀取速度,這是設計預讀指令的主要目的。如果當前運算複雜度比較高,那麼預取和運算就可同步進行,從而消除下一步內存訪問的時延。相應的預讀彙編指令有prefetch0、prefetch1、prefetch2、 prefetchnta。
預取指令只是給CPU一個提示,所以它可被CPU忽略,而且就算預取一段錯誤的地址也不會導致CPU異常。一般使用prefetchnta預取指令,因爲它不會污染CACHE,它把每次取得的數據都存放到L2 CACHE的第一條CACHE LINE,而另外幾條指令會替換CACHE中最近最少使用的CACHE LINE。

4.2.非暫時移動指令寫內存
我們知道爲了保證CACHE與內存之間的數據一致性,CPU對CACHE的寫操作主要有兩種方式同步到內存,寫透式(Write Through)和寫回式(Write-back)。不管哪種同步方式都是要消耗性能的,而在某些情況下,寫CACHE是不必要的:
有哪些情況不需要寫CACHE呢?比如做數據拷貝(高效memcpy函數實現)時,或者我們已經知道寫的數據在最近一段時間內(或者永遠)都不會再使用了,那麼此時就可以不用寫CACHE,讓對應的CACHE LINE自動失效,以便緩存其它數據。這在某些特殊場景非常有用,相應的彙編指令有movntq、movntsd、movntss、movntps、movntpd、movntdq、movntdqa。
完整的利用預讀指令和非暫時移動指令實現的高速內存拷貝函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void X_aligned_memcpy_sse2(void* dest, const void* src, const unsigned long size_t)
{
  __asm
  {
    mov esi, src;    //src pointer
    mov edi, dest;   //dest pointer
    mov ebx, size_t; //ebx is our counter
    shr ebx, 7;      //divide by 128 (8 * 128bit registers)
 
    loop_copy:
      prefetchnta 128[ESI]; //SSE2 prefetch
      prefetchnta 160[ESI];
      prefetchnta 192[ESI];
      prefetchnta 224[ESI];
 
      movdqa xmm0, 0[ESI]; //move data from src to registers
      movdqa xmm1, 16[ESI];
      movdqa xmm2, 32[ESI];
      movdqa xmm3, 48[ESI];
      movdqa xmm4, 64[ESI];
      movdqa xmm5, 80[ESI];
      movdqa xmm6, 96[ESI];
      movdqa xmm7, 112[ESI];
 
      movntdq 0[EDI], xmm0; //move data from registers to dest
      movntdq 16[EDI], xmm1;
      movntdq 32[EDI], xmm2;
      movntdq 48[EDI], xmm3;
      movntdq 64[EDI], xmm4;
      movntdq 80[EDI], xmm5;
      movntdq 96[EDI], xmm6;
      movntdq 112[EDI], xmm7;
 
      add esi, 128;
      add edi, 128;
      dec ebx;
 
      jnz loop_copy; //loop please
    loop_copy_end:
  }
}

總結
要高效的訪問內存,必須充分利用系統CACHE的緩存功能,因爲就目前來說,CACHE的訪問速度比內存快太多了。具體優化方法有:
1.用設計上壓縮結構體大小。
2.結構體儘量做到機器字(倍數)對齊。
3.結構體中頻繁訪問的字段儘量放在機器字對齊的位置。
4.頻繁讀寫的多個結構體變量儘量同時申請,使得它們儘可能的分佈在較小的線性空間範圍內,這樣可利用TLB緩衝。
5.當結構體比較大時,對結構體字段進行初始化或設置值時最好從第一個字段依次往後進行,這樣可保證對內存的訪問是順序進行。
6.額外的優化可以採用非暫時移動指令(如movntdq)與預讀指令(如prefetchnta)。
7.特殊情況可考慮利用多媒體指令SSE2、SSE4等。
當然,上面某些步驟之間存在衝突,比如壓縮結構體和結構體對齊,這就需要實際綜合考慮。

轉載請保留地址:http://www.lenky.info/archives/2011/11/310 或 http://lenky.info/?p=310


備註:如無特殊說明,文章內容均出自Lenky個人的真實理解而並非存心妄自揣測來故意愚人耳目。由於個人水平有限,雖力求內容正確無誤,但仍然難免出錯,請勿見怪,如果可以則請留言告之,並歡迎來討論。另外值得說明的是,Lenky的部分文章以及部分內容參考借鑑了網絡上各位網友的熱心分享,特別是一些帶有完全參考的文章,其後附帶的鏈接內容也許更直接、更豐富,而我只是做了一下歸納&轉述,在此也一併表示感謝。關於本站的所有技術文章,歡迎轉載,但請遵從CC創作共享協議,而一些私人性質較強的心情隨筆,建議不要轉載。

法律:根據最新頒佈的《信息網絡傳播權保護條例》,如果您認爲本文章的任何內容侵犯了您的權利,請以Email或書面等方式告知,本站將及時刪除相關內容或鏈接。

發佈了81 篇原創文章 · 獲贊 32 · 訪問量 70萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章