影響內(nèi)存訪問速度的因素主要有:
1.內(nèi)存帶寬:每秒讀寫內(nèi)存的數(shù)據(jù)量,由硬件配置決定。
2.CACHE高速緩沖:CPU與內(nèi)存之間的緩沖器,當(dāng)命中率比較高時(shí)能大大提供內(nèi)存平均訪問速度。
3.TLB轉(zhuǎn)換旁視緩沖:系統(tǒng)虛擬地址向物理地址轉(zhuǎn)換的高速查表機(jī)制,轉(zhuǎn)換速度比普通轉(zhuǎn)換機(jī)制要快。
我們能夠優(yōu)化的只有第2點(diǎn)和第3點(diǎn)。由于CACHE的小容量與SMP的同步競爭,如何最大限度的利用高速緩沖就是我們的明確優(yōu)化突破口(以常用的數(shù)據(jù)結(jié)構(gòu)體為例):
1.壓縮結(jié)構(gòu)體大?。横槍ACHE的小容量。
2.對結(jié)構(gòu)體進(jìn)行對齊:針對內(nèi)存地址讀寫特性與SMP上CACHE的同步競爭。
3.申請地址連續(xù)的內(nèi)存空間:針對TLB的小容量和CACHE命中。
4.其它優(yōu)化:綜合考慮多種因素
具體優(yōu)化方法
1.壓縮結(jié)構(gòu)體大小
系統(tǒng)CACHE是有限的,并且容量很小,充分壓縮結(jié)構(gòu)體大小,使得CACHE能緩存更多的被訪問數(shù)據(jù),無非是提高內(nèi)存平均訪問速度的有效方法之一。
壓縮結(jié)構(gòu)體大小除了需要我們對應(yīng)用邏輯做好更合理的設(shè)計(jì),盡量去除不必要的字段,還有一些額外針對結(jié)構(gòu)體本身的壓縮方法。
1.1.對結(jié)構(gòu)體字段進(jìn)行合理的排列
由于結(jié)構(gòu)體自身對齊的特性,具有同樣字段的結(jié)構(gòu)體,不同的字段排列順序會(huì)產(chǎn)生不同大小的結(jié)構(gòu)體。
大?。?2字節(jié)
1 2 3 4 5 6 7 |
struct box_a { char a; short b; int c; char d; }; |
大?。?字節(jié)
1 2 3 4 5 6 7 |
struct box_b { char a; char d; short b; int c; }; |
1.2.利用位域
實(shí)際中,有些結(jié)構(gòu)體字段并不需要那么大的存儲(chǔ)空間,比如表示真假標(biāo)記的flag字段只取兩個(gè)值之一,0或1,此時(shí)用1個(gè)bit位即可,如果使用int類型的單一字段就大大的浪費(fèi)了空間。
示例: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結(jié)構(gòu)體也是壓縮結(jié)構(gòu)體大小的方法之一,它允許我們在某些情況下能對結(jié)構(gòu)體的多個(gè)字段進(jìn)行合并或把小字節(jié)字段存放到大字節(jié)字段內(nèi)。
示例: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.對結(jié)構(gòu)體進(jìn)行對齊
對結(jié)構(gòu)體進(jìn)行對齊有兩層意思,一是指對較小結(jié)構(gòu)體進(jìn)行機(jī)器字對齊,二是指對較大結(jié)構(gòu)體進(jìn)行CACHE LINE對齊。
2.1.對較小結(jié)構(gòu)體進(jìn)行機(jī)器字對齊
我們知道,對于現(xiàn)代計(jì)算機(jī)硬件來說,內(nèi)存只能通過特定的對齊地址(比如按照機(jī)器字)進(jìn)行訪問。舉個(gè)例子來說,比如在64位的機(jī)器上,不管我們是要讀取第0個(gè)字節(jié)還是要讀取第1個(gè)字節(jié),在硬件上傳輸?shù)男盘柖际且粯拥?。因?yàn)樗紩?huì)把地址0到地址7,這8個(gè)字節(jié)全部讀到CPU,只是當(dāng)我們是需要讀取第0個(gè)字節(jié)時(shí),丟掉后面7個(gè)字節(jié),當(dāng)我們是需要讀取第1個(gè)字節(jié),丟掉第1個(gè)和后面6個(gè)字節(jié)。
當(dāng)我們要讀取的字節(jié)剛好落在兩個(gè)機(jī)器字內(nèi)時(shí),就出現(xiàn)兩次訪問內(nèi)存的情況,同時(shí)通過一些邏輯計(jì)算才能得到最終的結(jié)果。
因此,為了更好的提升性能,我們須盡量將結(jié)構(gòu)體做到機(jī)器字(或倍數(shù))對齊,而結(jié)構(gòu)體中一些頻繁訪問的字段也盡量安排在機(jī)器字對齊的位置。
大?。?2字節(jié)
1 2 3 4 5 6 7 8 |
struct box_c { char a; char d; short b; int c; int e; }; |
大?。?6字節(jié)
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結(jié)構(gòu)體,通過增加一個(gè)填充字段padding將結(jié)構(gòu)體大小增加到16字節(jié),從而與機(jī)器字倍數(shù)對齊,這在我們申請連續(xù)的box_d結(jié)構(gòu)體數(shù)組時(shí),仍能保證數(shù)組內(nèi)的每一個(gè)結(jié)構(gòu)體都與機(jī)器字倍數(shù)對齊。
通過填充字段padding使得結(jié)構(gòu)體大小與機(jī)器字倍數(shù)對齊是一種常見的做法,在Linux內(nèi)核源碼里隨處可見。
2.2.對較大結(jié)構(gòu)體進(jìn)行CACHE LINE對齊
我們知道,CACHE與內(nèi)存交換的最小單位為CACHE LINE,一個(gè)CACHE
LINE大小以64字節(jié)為例。當(dāng)我們的結(jié)構(gòu)體大小沒有與64字節(jié)對齊時(shí),一個(gè)結(jié)構(gòu)體可能就要占用比原本需要更多的CACHE
LINE。比如,把一個(gè)內(nèi)存中沒有64字節(jié)長的結(jié)構(gòu)體緩存到CACHE時(shí),即使該結(jié)構(gòu)體本身長度或許沒有還沒有64字節(jié),但由于其前后搭占在兩條CACHE
LINE上,那么對其進(jìn)行淘汰時(shí)就會(huì)淘汰出去兩條CACHE LINE。
這還不是最嚴(yán)重的問題,非CACHE
LINE對齊結(jié)構(gòu)體在SMP機(jī)器上容易引發(fā)名為錯(cuò)誤共享的CACHE問題。比如,結(jié)構(gòu)體T1和T2都沒做CACHE
LINE對齊,如果它們(T1后半部和T2前半部)在SMP機(jī)器上合占了同一條CACHE,如果CPU 0對結(jié)構(gòu)體T1后半部做了修改則將導(dǎo)致CPU 1的CACHE
LINE 1失效,同樣,如果CPU 1對結(jié)構(gòu)體T2前半部做了修改則也將導(dǎo)致CPU 0的CACHE LINE 1失效。如果CPU 0和CPU
1反復(fù)做相應(yīng)的修改則導(dǎo)致的不良結(jié)果顯而易見。本來邏輯上沒有共享的結(jié)構(gòu)體T1和T2,實(shí)際上卻共享了CACHE LINE
1,這就是所謂的錯(cuò)誤共享。
Linux源碼里提供了利用GCC的__attribute__擴(kuò)展屬性定義的宏來做這種對齊處理,在文件/linux-2.6.xx/include/linux/cache.h內(nèi)可以找到多個(gè)相類似的宏,比如:
1 |
#define ____cacheline_aligned __attribute__((__aligned__(SMP_CACHE_BYTES))) |
該宏可以用來修飾結(jié)構(gòu)體字段,作用是強(qiáng)制該字段地址與CACHE
LINE映射起始地址對齊。
看/linux-2.6.xx/drivers/net/e100.c內(nèi)結(jié)構(gòu)體nic的實(shí)現(xiàn),三個(gè)____cacheline_aligned修飾字段,表示強(qiáng)制這些字段與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字節(jié)空洞 */ struct net_device *netdev; struct pci_dev *pdev; /* 40字節(jié)空洞 */ 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字節(jié)空洞 */ 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; … } |
回到前面的問題,如果我們對結(jié)構(gòu)體T2的第一個(gè)字段加上____cacheline_aligned修飾,則該錯(cuò)誤共享即可解決。
2.3.只讀字段和讀寫字段隔離對齊
只讀字段和讀寫字段隔離對齊的目的就是為了盡量保證那些只讀字段和讀寫字段分別集中在CACHE的不同CACHE
LINE中。由于只讀字段幾乎不需要進(jìn)行更新,因而能在CACHE中得以穩(wěn)定的緩存,減少由于混合有讀寫字段導(dǎo)致的對應(yīng)CACHE
LINE的頻繁失效問題,以便提高效率;而讀寫字段相對集中在一起,這樣也能保證當(dāng)程序讀寫結(jié)構(gòu)體時(shí),污染的CACHE LINE條數(shù)也就相對的較少。
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.申請地址連續(xù)的內(nèi)存空間
隨著地址空間由32位轉(zhuǎn)到64位,頁內(nèi)存管理的目錄分級也越來越多,4級的目錄地址轉(zhuǎn)換也是一筆不小是開銷。硬件產(chǎn)商為我們提供了TLB緩沖,加速虛擬地址到物理地址的換算。但是,畢竟TLB是有限,對地址連續(xù)的內(nèi)存空間進(jìn)行訪問時(shí),TLB能得到更多的命中,同時(shí)CACHE高速緩沖命中的幾率也更大。
兩段代碼,實(shí)現(xiàn)同一功能,但第一種方法在實(shí)際使用中,內(nèi)存讀寫效率就會(huì)相對較好,特別是在申請的內(nèi)存很大時(shí)(未考慮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)的分頁機(jī)制,同樣能夠提升性能;因?yàn)橄啾扔谠久宽?K大小的分頁機(jī)制,應(yīng)用程序申請同樣大小的內(nèi)存,大頁面分頁機(jī)制需要的頁面數(shù)目更少,從而占用的TLB項(xiàng)目也更少,減少虛擬地址到物理地址的轉(zhuǎn)換次數(shù)的同時(shí),提高TLB的命中率,縮短每次轉(zhuǎn)換所需要的時(shí)間。因?yàn)榇蠖鄶?shù)操作系統(tǒng)在分配內(nèi)存時(shí)候都需要按頁對齊,所以大頁面分頁機(jī)制的缺點(diǎn)就是內(nèi)存浪費(fèi)相對比較嚴(yán)重。只有在物理內(nèi)存足夠充足的情況下,大頁面分頁機(jī)制才能夠體現(xiàn)出優(yōu)勢。
4.其它優(yōu)化
4.1.預(yù)讀指令讀內(nèi)存
提前預(yù)取內(nèi)存中數(shù)據(jù)到CACHE內(nèi),提高CACHE的命中率,加速內(nèi)存讀取速度,這是設(shè)計(jì)預(yù)讀指令的主要目的。如果當(dāng)前運(yùn)算復(fù)雜度比較高,那么預(yù)取和運(yùn)算就可同步進(jìn)行,從而消除下一步內(nèi)存訪問的時(shí)延。相應(yīng)的預(yù)讀匯編指令有prefetch0、prefetch1、prefetch2、
prefetchnta。
預(yù)取指令只是給CPU一個(gè)提示,所以它可被CPU忽略,而且就算預(yù)取一段錯(cuò)誤的地址也不會(huì)導(dǎo)致CPU異常。一般使用prefetchnta預(yù)取指令,因?yàn)樗粫?huì)污染CACHE,它把每次取得的數(shù)據(jù)都存放到L2
CACHE的第一條CACHE LINE,而另外幾條指令會(huì)替換CACHE中最近最少使用的CACHE LINE。
4.2.非暫時(shí)移動(dòng)指令寫內(nèi)存
我們知道為了保證CACHE與內(nèi)存之間的數(shù)據(jù)一致性,CPU對CACHE的寫操作主要有兩種方式同步到內(nèi)存,寫透式(Write
Through)和寫回式(Write-back)。不管哪種同步方式都是要消耗性能的,而在某些情況下,寫CACHE是不必要的:
有哪些情況不需要寫CACHE呢?比如做數(shù)據(jù)拷貝(高效memcpy函數(shù)實(shí)現(xiàn))時(shí),或者我們已經(jīng)知道寫的數(shù)據(jù)在最近一段時(shí)間內(nèi)(或者永遠(yuǎn))都不會(huì)再使用了,那么此時(shí)就可以不用寫CACHE,讓對應(yīng)的CACHE
LINE自動(dòng)失效,以便緩存其它數(shù)據(jù)。這在某些特殊場景非常有用,相應(yīng)的匯編指令有movntq、movntsd、movntss、movntps、movntpd、movntdq、movntdqa。
完整的利用預(yù)讀指令和非暫時(shí)移動(dòng)指令實(shí)現(xiàn)的高速內(nèi)存拷貝函數(shù):
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: } } |
總結(jié)
要高效的訪問內(nèi)存,必須充分利用系統(tǒng)CACHE的緩存功能,因?yàn)榫湍壳皝碚f,CACHE的訪問速度比內(nèi)存快太多了。具體優(yōu)化方法有:
1.用設(shè)計(jì)上壓縮結(jié)構(gòu)體大小。
2.結(jié)構(gòu)體盡量做到機(jī)器字(倍數(shù))對齊。
3.結(jié)構(gòu)體中頻繁訪問的字段盡量放在機(jī)器字對齊的位置。
4.頻繁讀寫的多個(gè)結(jié)構(gòu)體變量盡量同時(shí)申請,使得它們盡可能的分布在較小的線性空間范圍內(nèi),這樣可利用TLB緩沖。
5.當(dāng)結(jié)構(gòu)體比較大時(shí),對結(jié)構(gòu)體字段進(jìn)行初始化或設(shè)置值時(shí)最好從第一個(gè)字段依次往后進(jìn)行,這樣可保證對內(nèi)存的訪問是順序進(jìn)行。
6.額外的優(yōu)化可以采用非暫時(shí)移動(dòng)指令(如movntdq)與預(yù)讀指令(如prefetchnta)。
7.特殊情況可考慮利用多媒體指令SSE2、SSE4等。
當(dāng)然,上面某些步驟之間存在沖突,比如壓縮結(jié)構(gòu)體和結(jié)構(gòu)體對齊,這就需要實(shí)際綜合考慮。
轉(zhuǎn)載請保留地址:http:///2011/11/23/%e5%a6%82%e4%bd%95%e9%ab%98%e6%95%88%e7%9a%84%e8%ae%bf%e9%97%ae%e5%86%85%e5%ad%98/
備注:如無特殊說明,文章內(nèi)容均出自Lenky個(gè)人的真實(shí)理解而并非存心妄自揣測來故意愚人耳目。由于個(gè)人水平有限,雖力求內(nèi)容正確無誤,但仍然難免出錯(cuò),請勿見怪,如果可以則請留言告之,并歡迎來信討論。另外值得說明的是,Lenky的部分文章以及部分內(nèi)容參考借鑒了網(wǎng)絡(luò)上各位網(wǎng)友的熱心分享,在此也一并表示感謝。關(guān)于本站的所有技術(shù)文章,歡迎轉(zhuǎn)載,但請遵從CC創(chuàng)作共享協(xié)議,而一些私人性質(zhì)較強(qiáng)的心情隨筆,建議不要轉(zhuǎn)載。
法律:根據(jù)最新頒布的《信息網(wǎng)絡(luò)傳播權(quán)保護(hù)條例》,如果您認(rèn)為本文章的任何內(nèi)容侵犯了您的權(quán)利,請以Email或書面等方式告知,本站將及時(shí)刪除相關(guān)內(nèi)容或鏈接。





