持續(xù)更新中笆凌。
Memory Access
Address Alignment
在內(nèi)存中存取一個(gè)變量最高效的方式是將其放在一個(gè)可以被它的長度整除的地址上蕴侣。
(void *)&variable % sizeof(variable) == 0
所謂的按某個(gè)長度對齊就是這個(gè)意思捣炬。GCC編譯器會自動幫我們處理這些事情蛋欣。比較特殊的方式是將一個(gè)大型的結(jié)構(gòu)體丐巫,或者靜態(tài)數(shù)組按64byte的方式對齊:
int BigArray[1024] __attribute__((aligned(64)));
這主要是考慮到CPU的Cache Line長度多為64byte锨侯,變量按64對齊可以使其正好開始于一個(gè)Cache Line,減少Cache Miss/False Sharing以及利用CPU的高級指令集并行計(jì)算捂刺。
Note: _ attribute _((aligned(x)))有時(shí)只對全局變量有效谣拣,而對局部變量無效。
Huge Page
大頁技術(shù)是當(dāng)前流行的一種性能優(yōu)化技術(shù)族展。在Linux系統(tǒng)中有一套復(fù)雜的進(jìn)程虛擬地址和內(nèi)存物理地址的轉(zhuǎn)換機(jī)制森缠,復(fù)雜的細(xì)節(jié)我們不去關(guān)心,只需要知道Linux是通過頁(Page)這一機(jī)制(Look-up table)來確立兩者的對應(yīng)關(guān)系的仪缸。簡單的類比就是在一本2000頁的書中找到某一個(gè)章節(jié)贵涵,遠(yuǎn)比在一本2頁的書中復(fù)雜∏』考慮到傳統(tǒng)頁面4KB的大小和大頁2GB的小大之差宾茂,這個(gè)類比還不是那么恰當(dāng)。
在CPU中拴还,需要以緩存的形式存儲一些轉(zhuǎn)換關(guān)系跨晴,這種緩存成為TLB Cache。使用大頁可以減少TLB Cache Miss片林。
Virtual addr maps to physical addr
Note: Huge Page可以在絕大部分情況之下提高性能端盆,但并不是所有情況下都可以起到提升性能的效果树瞭。對于內(nèi)存,需要綜合考慮各種因素爱谁,提高性能的基本策略還是以空間換時(shí)間。詳細(xì)的分析文章請Click
NUMA
嚴(yán)格來說NUMA并不是一種性能優(yōu)化技術(shù)孝偎,而是一種內(nèi)存架構(gòu)访敌。
NUMA Architecture
每一個(gè)CPU Core都與它本地連接的內(nèi)存直接相連,獨(dú)享總線衣盾,具有最快的讀寫速度寺旺。如果去遠(yuǎn)程(remote)內(nèi)存去讀寫的話,則需要跨CPU Core執(zhí)行势决。在DPDK中阻塑,有一整套精巧且高效的內(nèi)存分配和管理機(jī)制,結(jié)合大頁和NUMA等機(jī)制果复,基本原則是陈莽,將一個(gè)CPU Core需要處理的數(shù)據(jù)都放在離它最近的內(nèi)存上。
相關(guān)的實(shí)現(xiàn)可以參考DPDK中memseg
memzone
等內(nèi)存機(jī)制相關(guān)代碼的實(shí)現(xiàn)虽抄,這里可以有專門文章介紹走搁。
Polling Mode Drive(PMD)
是DPDK實(shí)現(xiàn)的優(yōu)化Linux網(wǎng)絡(luò)接收發(fā)送性能的模塊,官方有詳細(xì)的介紹資料迈窟。Click
Memory Pool
對于需要頻繁填充釋放的內(nèi)存空間可以采用內(nèi)存池的方式預(yù)先動態(tài)分配一整塊內(nèi)存區(qū)域,然后統(tǒng)一進(jìn)行管理,從而省去頻繁的動態(tài)分配和釋放過程锄贼,既提高了性能榴都,同時(shí)也減少了內(nèi)存碎片的產(chǎn)生。
內(nèi)存池多以隊(duì)列的形式組織空閑或占用內(nèi)存湖员。在DPDK中贫悄,還考慮了地址對齊,以及CPU core local cache等因素破衔,以提升性能清女。
這里提到的內(nèi)存對齊不同于前面僅僅將變量放在一個(gè)合適的地址“數(shù)目”上,而是綜合考慮了內(nèi)存通道(channel)和rank晰筛,將變量(比如一個(gè)三層網(wǎng)絡(luò)的Pkt)嫡丙,平均分布于不同的channel之上(多依靠padding),可以減少channel擁塞读第,顯著提升性能曙博。如圖:
Two Channels and Quad-ranked DIMM Example
對于供多個(gè)線程同時(shí)使用的內(nèi)存池,為了減少對內(nèi)存池的讀寫沖突怜瞒,可以考慮Local Cache的機(jī)制父泳。即內(nèi)存池為每一個(gè)線程/CPU Core維護(hù)一個(gè)Local Cache般哼,本地的CPU Core對其操作是沒有競爭的。每個(gè)CPU Core都是以bulk的形式從內(nèi)存池中請求數(shù)據(jù)寫入Local Cache或者將Local Cache的數(shù)據(jù)寫入內(nèi)存池惠窄。這樣便大幅減少了讀寫沖突蒸眠。
Local cache per core
Linker Considerations
可以理解的一個(gè)簡單事實(shí)是,如果經(jīng)常使用的函數(shù)被儲存在指令內(nèi)存的同一區(qū)域杆融,甚至存儲順序和調(diào)用順序一致楞卡,那么程序整體執(zhí)行的效率將會有所提升。
一個(gè)簡單的方法就是盡量使用static
函數(shù):
static void f(void)
將同一模塊中的函數(shù)在鏈接階段放在一起脾歇。但很多時(shí)候蒋腮,處于程序模塊化編程考慮,模塊之間互相調(diào)用的函數(shù)和方法并沒有被顯式地置于同一處指令內(nèi)存藕各,此時(shí)可以在關(guān)鍵函數(shù)集中采用:
_attaribute_(section(X))
將函數(shù)顯式地置于Read only(program memory)Section X池摧,方便一起調(diào)用。同時(shí)也可以map文件的形式安排指定激况。
Note: 同樣的原則也適用于變量作彤。
CPU
Advanced Instruction Set
使用先進(jìn)的CPU指令集,帶來的主要好處是可以并行完成向量化的操作乌逐,也就是所謂的SIMD(Single-Instruction-Multiple-Data)操作宦棺。
當(dāng)需要對大型數(shù)據(jù)集執(zhí)行相同的操作的時(shí)候,向量操作可以帶來明顯的性能提升黔帕。例如圖像處理代咸、大型矩陣計(jì)算、網(wǎng)絡(luò)數(shù)據(jù)包還有內(nèi)存復(fù)制操作等成黄。
Note: 對于數(shù)據(jù)間有互相依賴和操作上有繼承的運(yùn)算呐芥,比如排序,并不適合向量操作奋岁。
先進(jìn)的指令集一般包括SSE
SSE2
AVX
AVX512
YMM
ZMM
思瘟。這些指令集對數(shù)據(jù)儲存的地址都有比較嚴(yán)格的要求,比如256bit-YMM
要求數(shù)據(jù)按32對齊闻伶,512-bit ZMM
要求數(shù)據(jù)按64對齊滨攻。
對于向量操作,一般希望符合如下條件:
- 小型的數(shù)據(jù)類型:
char
short
int
float
- 對大型數(shù)據(jù)集執(zhí)行類似的操作
- 數(shù)據(jù)對齊
- 數(shù)據(jù)集長度可以被向量長度整除
Note: 可以將關(guān)鍵函數(shù)寫為針對不同數(shù)據(jù)集的不同版本蓝翰,視運(yùn)行環(huán)境編譯運(yùn)行光绕。
Compiler
Branch Predication
CPU是以流水線的方式執(zhí)行程序指令。所謂流水線畜份,可以簡單理解為在執(zhí)行一個(gè)指令的同時(shí)诞帐,讀取下一條指令。對于程序中大量出現(xiàn)的
if else
while
for
? :
等含有條件判斷的情景爆雹,CPU需要能夠正確提取下一條指令以便流水線可以流暢執(zhí)行下去停蕉。一旦提取的是錯(cuò)誤分支的指令愕鼓,雖然不影響程序運(yùn)行的結(jié)果,但整條流水線都會被清空慧起,再重新讀入正確分支的指令菇晃,對程序運(yùn)行效率影響頗大。CPU一般都有硬件分支預(yù)測器蚓挤,但我們也可以用
likely()/unlikely()
等方式顯示指定谋旦,另外在設(shè)計(jì)程序的時(shí)候也以使分支判斷具有一定的規(guī)律性為好,比如一組經(jīng)過排序的輸入數(shù)據(jù)屈尼。
Branchless Code
為了最大限度減小Branch mispredication對性能帶來的影響,可以將一些常見的分支判斷轉(zhuǎn)換為Branchless的形式拴孤。比如返回兩個(gè)數(shù)中較大的值脾歧,一般可以寫做:
int max = (x > y) ? x : y;
這里其實(shí)隱含了一個(gè)條件判斷。如果用branchless的形式演熟,同樣的功能可以寫做為:
int max = x ^ ((x ^ y) & -(x < y));
當(dāng)有大量調(diào)用鞭执,同時(shí)輸入無甚規(guī)律性的時(shí)候可以考慮采用Branchless code。一個(gè)比較全面的技巧合計(jì)在:Click芒粹。
loop-unrolling
Loop-unrolling的一大好處就是可以減少循環(huán)分支預(yù)測的次數(shù)兄纺。對于簡單的循環(huán),CPU其實(shí)可以很好的完成分支預(yù)測的工作化漆,但對于嵌套的循環(huán)估脆,或者循環(huán)內(nèi)部會改變循環(huán)次數(shù)的循環(huán),分支預(yù)測就變得困難座云。loop-unrolling的特點(diǎn)可以用如下的例子說明:
int i;
for (i = 0; i < 20; i++) {
if (i % 2 == 0) {
FuncA(i);
} else {
FuncB(i);
}
FuncC(i);
}
上面這個(gè)執(zhí)行了20次的循環(huán)可以用loop-unrolling展開:
int i;
for (i = 0; i < 20; i += 2) {
FuncA(i);
FuncC(i);
FuncB(i + 1);
FuncC(i + 1);
}
Cons:
循環(huán)只執(zhí)行10次疙赠,減少了一半
可以更準(zhǔn)確得被CPU的branch predictor預(yù)測
沒有了循環(huán)體內(nèi)的
if
分支Pros
如果循環(huán)計(jì)數(shù)器是奇數(shù),則需要特別的處理朦拖。
Note: Loop-unrolling也需要考慮適用場合圃阳。主要適用于循環(huán)體的分支是主要的性能熱點(diǎn)的時(shí)候。
Anti-aliasing
當(dāng)有多個(gè)指針指向同一處物理內(nèi)存(變量)的時(shí)候璧帝,稱為pointer aliasing捍岳。作為編譯器,并不能確認(rèn)兩個(gè)相同類型的指針是否指向同一處地址睬隶,即對其他指針的操作锣夹,是否會影響另外的指針?biāo)赶虻膬?nèi)存。這就要求每次碰到這兩個(gè)指針其中的任何一個(gè)的時(shí)候苏潜,都需要重新從內(nèi)存中讀取當(dāng)前值晕城。示例如下:
void Func1 (int a[], int *p) {
int i;
for (i = 0; i < 100; i++) {
a[i] = *p + 2;
}
}
void Func2() {
int list[100];
Func1(list, &list[8]);
}
在Func1
中,有必要每次都重新讀入*p
窖贤,并且重新計(jì)算*p + 2
砖顷,因?yàn)樵?code>Func2的調(diào)用中贰锁,與list[8]
發(fā)生aliasing。對編譯器來講滤蝠,它需要考慮這種“理論上的可能”豌熄,從而付出大量的重復(fù)勞動。
當(dāng)程序可以確認(rèn)兩個(gè)指針不會發(fā)生aliasing的時(shí)候物咳,可以用關(guān)鍵字__restrict__
給編譯器以明確的指示锣险。
Prefetch
使用prefetch指令可以幫助我們提前預(yù)存一個(gè)將要使用的變量至CPU緩存:
_mm_prefetch
但在實(shí)際使用過程中要特別小心,現(xiàn)代CPU都有自己的硬件prefetch機(jī)制览闰,如果不是經(jīng)過測試芯肤,確認(rèn)性能有所提高,盡量不要輕易使用該指令压鉴。這里有一篇資料對此有詳細(xì)解釋:Click
Note:需要確認(rèn)CPU支持SSE指令集
Multi-threads
Lock-less
一般將GCC提供的一些原子操作視為“Lock-less code”崖咨。這些操作包括一些原子自增,CAS等操作油吭。
__sync_fetch_and_add(type* ptr, type value)
__sync_compare_and_swap(type *ptr, type oldval, type newval)
etc...
這些操作雖然表面上沒有了鎖的痕跡击蹲,但實(shí)際上其匯編指令還是存在一個(gè)#lock鎖總線的操作。所以也不必對其性能抱太高期望婉宰。對于所有關(guān)于鎖的操作歌豺,需要強(qiáng)調(diào)的是,鎖本身并不影響性能心包,只有對鎖的爭搶才影響性能类咧。
Local Cache
如同之前介紹的那樣,還有一種策略是將任務(wù)盡量劃分為不相互依賴的各部分蟹腾,分別交給不同的CPU Core去處理轮听,僅僅在結(jié)果匯總的時(shí)候有少量的鎖操作。在DPDK中大量應(yīng)用了這種思想岭佳。
Core Affinity
將一個(gè)任務(wù)指定交給某個(gè)CPU Core處理血巍,可以減少上下文切換和context switch的次數(shù),以及提高緩存命中率珊随。在Linux程序中可以通過
int sched_setaffinity(pid_t pid, size_t cpusetsize, const cpu_set_t *mask);
來設(shè)定線程的CPU親和性述寡。
False Sharing
False Sharing也是在多線程操作中需要避免的緩存失效的問題。如果兩個(gè)變量分別被兩個(gè)線程操作叶洞,但它們出現(xiàn)在同一條Cache Line中鲫凶,則兩個(gè)線程之間還是會互相影響。任何一個(gè)線程對該Cache Line的寫操作衩辟,都會失整條Cache Line在另外一個(gè)線程處失效螟炫。如下圖:
對此最簡單的辦法,是可以添加Cache Padding將兩個(gè)變量分隔在不同的Cache Line之中艺晴,或者以Cache Line Size對齊的方式分配內(nèi)存昼钻。