轉自:http://blog.csdn.net/zhang_shuai_2011/article/details/38119657
原文如下:
一. Cache
Cache一般來說,需要關心以下幾個方面
1)Cache hierarchy
Cache的層次,一般有L1, L2, L3 (L是level的意思)的cache。通常來說L1,L2是集成 在CPU里面的(可以稱之為On-chip cache),而L3是放在CPU外面(可以稱之為Off-chip cache)。當然這個不是絕對的,不同CPU的做法可能會不太一樣。這里面應該還需要加上register,雖然register不是cache,但是把數據放到register里面是能夠提高性能的。
2)Cache size
Cache的容量決定了有多少代碼和數據可以放到Cache里面,有了Cache才有了競爭,才有了替換,才有了優化的空間。如果一個程序的熱點(hotspot)已經完全填充了整Cache,那么再從Cache角度考慮優化就是白費力氣了,巧婦難為無米之炊。我們優化程序的目標是把程序盡可能放到Cache里面,但是把程序寫到能夠占滿整個Cache還是有一定難度的,這么大的一個Code path,相應的代碼得有多少,代碼邏輯肯定是相當的復雜(基本上是不可能,至少我沒有見過)。
3)Cache line size
CPU從內存load數據是一次一個cache line;往內存里面寫也是一次一個cache line,所以一個cache line里面的數據最好是讀寫分開,否則就會相互影響。
4)Cache associative
Cache的關聯。有全關聯(full associative),內存可以映射到任意一個Cache line;也有N-way關聯,這個就是一個哈希表的結構,N就是沖突鏈的長度,超過了N,就需要替換。
5)Cache type
有I-cache(指令cache),D-cache(數據cache),TLB(MMU的cache),每一種又有L1,L2等等,有區分指令和數據的cache,也有不區分指令和數據的cache。
二. 代碼層次的優化
1) 字節 alignment (字節對齊)
要理解字節對齊,首先得理解系統內存的組織結構. 把1個內存單元稱為1個字節,字節再組成字,在8086時代,16位的機器中1字=2個字節=16bit,而80386以后的32位系統中,1字=4個字節。大多數計算機指令都是對字進行操作,如將兩字相加等。也就是說,32位CPU的寄存器為32位,導致指令的操作對象是32位字;16位CPU的寄存器為16位,移動、加、減等指令的操作對象也是16位字。由於指令的原因,內存的尋址也同樣是按字進行操作,在16位系統中,如果你訪問的只是低8位,內存尋址還是按16位進行,然后再根據A0地址線選擇低8位還是高8位,這一過程成為一次內存讀(寫),在16位系統中,如果讀取一個32位數,要花費兩個內存讀周期(先讀低16,再讀高16)。同理32位CPU的內存尋址按4個單元進行。
為了達到高效的目的,在16位系統中,變量存儲的起始地址是2的倍數,32位系統中,變量存儲的起始地址是4的倍數,而這些工作都是由編譯器來完成的。下面舉個例子來說明這個問題。如下圖所示:緩存對齊與字節對齊 - CR7 - CR7的博客
上圖是16位系統的內存布局圖,深藍色表示變量覆蓋的內存范圍,假設變量的大小為2個字節,變量的起始物理內存地址為0000H時,訪問這個變量時,只需要一次內存的讀寫。然而,當變量的內存起始地址為0001H時,cpu將耗費兩次讀周期進行變量訪問,具體過程如下:為了訪問變量的低8位,cpu將通過尋址訪問起始地址為0000H所在的字,然后找到當前字的高8位;隨后cpu再訪問0002H所處字的低8位,此低8位就是變量的高8位,這樣經過cpu的拼裝變量的訪問就結束了,可見,需要經過兩次讀周期才能正確訪問變量的值,效率是前者的1/2。
__attribute__((aligned(n)))表示所定義的變量為n字節對齊;
字節對齊的細節和編譯器實現相關,但一般而言,滿足三個准則:
1) (結構體)變量的首地址能夠被其(最寬)基本類型成員的大小所整除;
2) 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(internal adding);
3) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之后加上填充字節(trailing padding)。
__attribute__ ((packed)) 的作用就是告訴編譯器取消結構在編譯過程中的優化對齊,按照實際占用字節數進行對齊,是GCC特有的語法。這個功能是跟操作系統沒關系,跟編譯器有關,gcc編譯器不是緊湊模式的.例如:
__attribute__ ((packed)) 的作用就是告訴編譯器取消結構在編譯過程中的優化對齊,按照實際占用字節數進行對齊,是GCC特有的語法。
__attribute__((aligned(n)))表示所定義的變量為n字節對齊;
struct B{ char b;int a;short c;}; (默認4字節對齊)
這時候同樣是總共7個字節的變量,但是sizeof(struct B)的值卻是12。
下面我們使用預編譯指令__attribute__((aligned(n)))來告訴編譯器,使用我們指定的對齊值來取代缺省的:
struct C{char b;int a;short c;}; __attribute__((aligned(2)))
這時候同樣是總共7個字節的變量,但是sizeof(struct B)的值卻是8
struct D{ char b;int a;short c;}; __attribute__ ((packed))
sizeof(struct C)值是8,sizeof(struct D)值為7。
字節對齊的細節和編譯器實現相關,但一般而言,滿足三個准則:
1) (結構體)變量的首地址能夠被其(最寬)基本類型成員的大小所整除;
2) 結構體每個成員相對於結構體首地址的偏移量(offset)都是成員大小的整數倍,如有需要編譯器會在成員之間加上填充字節(internal adding);
3) 結構體的總大小為結構體最寬基本類型成員大小的整數倍,如有需要編譯器會在最末一個成員之后加上填充字節(trailing padding)。
2) Cache line alignment (cache對齊)
數據跨越兩個cache line,就意味着兩次load或者兩次store。如果數據結構是cache line對齊的, 就有可能減少一次讀寫。數據結構的首地址cache line對齊,意味着可能有內存浪費(特別是 數組這樣連續分配的數據結構),所以需要在空間和時間兩方面權衡。
對於普通代碼,內存邊界對齊也是有好處的,可以降低高速緩存(Cache)和內存交換數據的次數。 主要問題是在於Cache本身是分成很多Cache-Line,每條Cache-Line具有一定的長度,比如一般來說L1 Cache每條Cache Line長度在32個字節或64個字節;而L2的會更大,比如64個字節或128個字節。用戶每次訪問地址空間中一個變量,如果不在Cache當中,那么就需要從內存中先將數據調入Cache中.
比如現在有個變量 int x;占用4個字節,它的起始地址是0x1234567F;那么它占用的內存范圍就在0x1234567F-0x12345682之間。如果現在Cache Line長度為32個字節,那么每次內存同Cache進行數據交換時,都必須取起始地址時32(0x20)倍數的內存位置開始的一段長度為32的內存同Cache Line進行交換. 比如0x1234567F落在范圍0x12345660~0x1234567F上,但是0x12345680~0x12345682落在范圍 0x12345680~0x1234569F上,也就是說,為了將4個字節的整數變量0x1234567F~0x12345682裝入Cache,我們必 須調入兩條Cache Line的數據。但是如果int x的起始地址按4的倍數對齊,比如是 0x1234567C~0x1234567F,那么必然會落在一條Cache Line上,所以每次訪問變量x就最多只需要裝入一條Cache Line的數據了。比如現在一般的malloc()函數,返回的內存地址會已經是8字節對齊的,這個就是為了能夠讓大部分程序有更好的性能。
1. __attribute__((aligned(cache_line)))對齊實現;
struct syn_str { ints_variable; };__attribute__((aligned(cache_line)));
2. 算法實現
引子
int a;
int size = 8; <----> 1000(bin)
計算a以size為倍數的下界數:
就讓這個數(要計算的這個數)表示成二進制時,最后三位為0就可以達到這個目標。只要下面這個數與a進行"與運算"就可以了:
11111111 11111111 11111111 11111000
而上面這個數實際下就是 ~(size - 1),可以將該數稱為size的對齊掩碼size_mask.
計算a以size為倍數的上下界數:
#define alignment_down(a, size) (a & (~(size-1)) )
#define alignment_up(a, size) ((a+size-1) & (~ (size-1)))
注: 上界數的計算方法,如果要求出比a大的是不是需要加上8就可以了?可是如果a本身就是8的倍數,這樣加8不就錯了嗎,所以在a基礎上加上(size - 1), 然后與size的對齊掩碼進行與運算.
例如:
a=0, size=8, 則alignment_down(a,size)=0, alignment_up(a,size)=0.
a=6, size=8, 則alignment_down(a,size)=0, alignment_up(a,size)=8.
a=8, size=8, 則alignment_down(a,size)=8, alignment_up(a,size)=8.
a=14, size=8,則alignment_down(a,size)=8, alignment_up(a,size)=16.
注:size應當為2的n次方, 即2, 4, 8, 16, 32, 64, 128, 256, 1024, 2048, 4096 ...
實現例子:
struct syn_str { int s_variable; };
void *p = malloc ( sizeof (struct syn_str) + cache_line );
syn_str *align_p=(syn_str*)((((int)p)+(cache_line-1))&~(cache_line-1);
3) Branch prediction (分支預測)
代碼在內存里面是順序排列的。對於分支程序來說,如果分支語句之后的代碼有更大的執行幾率, 那么就可以減少跳轉,一般CPU都有指令預取功能,這樣可以提高指令預取命中的幾率。分支預測 用的就是likely/unlikely這樣的宏,一般需要編譯器的支持,這樣做是靜態的分支預測。現在也有 很多CPU支持在CPU內部保存執行過的分支指令的結果(分支指令的cache),所以靜態的分支預測 就沒有太多的意義。如果分支是有意義的,那么說明任何分支都會執行到,所以在特定情況下,靜態 分支預測的結果並沒有多好,而且likely/unlikely對代碼有很大的侵害(影響可讀性),所以一般不 推薦使用這個方法.
if(likely(value)) 等價於 if(value)
if(unlikely(value)) 也等價於 if(value)
也就是說 likely() 和 unlikely() 從閱讀和理解代碼的角度來看,是一樣的!!!
這兩個宏在內核中定義如下:
#define likely(x) __builtin_expect((x),1)
#define unlikely(x) __builtin_expect((x),0)
__builtin_expect() 是 GCC (version >= 2.96)提供給程序員使用的,目的是將“分支轉移”的信息提供給編譯器,這樣編譯器可以對代碼進行優化,以減少指令跳轉帶來的性能下降。
__builtin_expect((x),1) 表示 x 的值為真的可能性更大;
__builtin_expect((x),0) 表示 x 的值為假的可能性更大。
也就是說,使用 likely() ,執行 if 后面的語句 的機會更大,使用unlikely(),執行else 后面的語句的機會更大。
例如下面這段代碼,作者就認為 prev 不等於 next 的可能性更大,
if (likely(prev != next)) {
next->timestamp = now;
...
} else {
...;
}
通過這種方式,編譯器在編譯過程中,會將可能性更大的代碼緊跟着起面的代碼,從而減少指令跳轉帶來的性能上的下降。
下面以兩個例子來加深這種理解:
第一個例子: example1.c
int testfun(int x)
{
if(__builtin_expect(x, 0)) {
^^^--- We instruct the compiler, "else" block is more probable
x = 5;
x = x * x;
} else {
x = 6;
}
return x;
}
在這個例子中,我們認為 x 為0的可能性更大
編譯以后,通過 objdump 來觀察匯編指令,在我的 2.4 內核機器上,結果如下:
# gcc -O2 -c example1.c
# objdump -d example1.o
Disassembly of section .text:
00000000 <testfun>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 85 c0 test %eax,%eax
8: 75 07 jne 11 <testfun+0x11>
a: b8 06 00 00 00 mov $0x6,%eax
f: c9 leave
10: c3 ret
11: b8 19 00 00 00 mov $0x19,%eax
16: eb f7 jmp f <testfun+0xf>
可以看到,編譯器使用的是 jne (不相等跳轉)指令,並且 else block 中的代碼緊跟在后面。
8: 75 07 jne 11 <testfun+0x11>
a: b8 06 00 00 00 mov $0x6,%eax
第二個例子: example2.c
int testfun(int x)
{
if(__builtin_expect(x, 1)) {
^^^ --- We instruct the compiler, "if" block is more probable
x = 5;
x = x * x;
} else {
x = 6;
}
return x;
}
在這個例子中,我們認為 x 不為 0 的可能性更大
編譯以后,通過 objdump 來觀察匯編指令,在我2.4內核機器上,結果如下:
# gcc -O2 -c example2.c
# objdump -d example2.o
Disassembly of section .text:
00000000 <testfun>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 85 c0 test %eax,%eax
8: 74 07 je 11 <testfun+0x11>
a: b8 19 00 00 00 mov $0x19,%eax
f: c9 leave
10: c3 ret
11: b8 06 00 00 00 mov $0x6,%eax
16: eb f7 jmp f <testfun+0xf>
這次編譯器使用的是 je (相等跳轉)指令,並且 if block 中的代碼緊跟在后面。
8: 74 07 je 11 <testfun+0x11>
a: b8 19 00 00 00 mov $0x19,%eax