0x01 前言
系統(tǒng)里面經(jīng)常需要大量地搬運(yùn)數(shù)據(jù),一般調(diào)用的都是memcpy() C庫來實(shí)現(xiàn)括堤,因此本著“揪牛角尖”的精神厉熟,我們就來探究探究加速方案廉丽!畢竟很多事情被分解到底層之后就是一樣的呢!
加速這個(gè)玩意弄兜,其實(shí)是跟很多因素相關(guān)的药蜻,因此我們要就環(huán)境來論加速瓷式,把當(dāng)前環(huán)境考慮進(jìn)去然后再設(shè)計(jì)出合理的優(yōu)化策略,這才是萬全之策谷暮!也即魯棒性很強(qiáng)的策略蒿往。
這里我們從上而下地談?wù)刴emcpy的優(yōu)化問題,一個(gè)問題可以如此優(yōu)化湿弦,那么相比其他問題也都是類似罷瓤漏!這怕就是深度學(xué)習(xí)之遷移學(xué)習(xí)的由來了!這個(gè)世界真奇妙颊埃!
0x02 測(cè)試環(huán)境
我們?cè)贏RM Cortex-A8 環(huán)境下進(jìn)行一系列的測(cè)試蔬充。
測(cè)試時(shí)基于源/終地址以及我們要讀寫的byte數(shù)都是L1(64byte)的倍數(shù);
我們需要考慮下對(duì)齊班利,但是這里只測(cè)了16MB饥漫,影響不大故不考慮;
測(cè)試時(shí)間是由處理器內(nèi)部的性能寄存器記錄的罗标;
所有的測(cè)試中庸队,L1 NEON 位被激活,這意味著當(dāng)我們使用Neon 加載(load)指令的時(shí)候闯割,會(huì)使得L1數(shù)據(jù)緩存進(jìn)行l(wèi)inefill操作彻消;
我們把分別對(duì)應(yīng)指令和數(shù)據(jù)的L1、L2緩存使能宙拉,同時(shí)MMU核分支預(yù)測(cè)同樣也被使能了宾尚。
有些地方甚至還使用到了PLD預(yù)取指令:這條指令會(huì)使得L2緩存在這個(gè)數(shù)據(jù)被使用前的某個(gè)時(shí)間開始加載數(shù)據(jù),它提前發(fā)出了內(nèi)存請(qǐng)求谢澈,所以CPU就不需要在那傻等memory把數(shù)據(jù)給吐出來了煌贴!
0x03 測(cè)試策略
策略1: 傻搬
傻搬就是使用常規(guī)匯編指令一個(gè)字一個(gè)字地拷貝。
如下匯編代碼所示锥忿, 我們每次把地址值加個(gè)4牛郑,然后不斷循環(huán)直至搬運(yùn)結(jié)束,這是最基本最常規(guī)的一種操作了敬鬓,因此我們 把這個(gè)時(shí)間作為一個(gè)baseline.
匯編代碼如下:
WordCopy
LDR r3, [r1], #4
STR r3, [r0], #4
SUBS r2, r2, #4
BGE WordCopy
策略2: 多加載指令
我們之前是只用到了LDR指令井濒,一次搬運(yùn)32bit也就是1個(gè)word,由于只用到了r0~r3寄存器列林,因此每次調(diào)用的時(shí)候無需進(jìn)行堆棧操作瑞你;
這里我們使用LDM核STM指令(M是Multiple的簡(jiǎn)多,意味著一次操作多個(gè))希痴,每個(gè)迭代過程中能夠操作8word數(shù)據(jù)者甲,由于額外寄存器的使用,因此我們需要有現(xiàn)場(chǎng)保護(hù)操作砌创,也就是入棧出棧操作虏缸。
LDMCopy
PUSH {r4-r10}
LDMloop
LDMIA r1!, {r3 - r10}
STMIA r0!, {r3 - r10}
SUBS r2, r2, #32
BGE LDMloop
POP {r4-r10}
注:
1鲫懒、 r0~r3一般作為函數(shù)的局部變量,傳入的函數(shù)的參數(shù)按照順序分給他們四個(gè)刽辙,超出的就要進(jìn)入堆棧區(qū)了窥岩,其中r0一般還會(huì)作為函數(shù)返回值變量。
2宰缤、 r1!表示從r1這個(gè)地址處連續(xù)搬數(shù)據(jù)至r3到r10颂翼,每搬一個(gè),r1的值就會(huì)自動(dòng)加4慨灭。
策略3: NEON搬運(yùn)
常規(guī)的NEON搬運(yùn)朦乏,具體指令啊信息啊什么的,怎么操作啊氧骤,去看我上一篇博客吧呻疹!
NEONCopy
VLDM r1!, {d0-d7}
VSTM r0!, {d0-d7}
SUBS r2, r2, #0x40
BGE NEONCopy
這里一個(gè)d寄存器就是64bit,2個(gè)word筹陵,8個(gè)d寄存器搬運(yùn)的是16個(gè)word了肮舸浮!報(bào)告老板朦佩,有人開掛并思!
策略4: 傻搬+預(yù)取
如題。
- PLD的意思是我從r1地址處開始預(yù)先取出256byte數(shù)據(jù)到cache里面吕粗;
- r12表示的是 我要搬運(yùn)16次,每次都是4個(gè)byte旭愧,共計(jì)搬運(yùn)64byte每輪颅筋;
- 然后每輪結(jié)束后,把r2中的計(jì)數(shù)器減去64byte(0x40)開始下一輪直至結(jié)束输枯;
WordCopyPLD
PLD [r1, #0x100]
MOV r12, #16
WordCopyPLD1
LDR r3, [r1], #4
STR r3, [r0], #4
SUBS r12, r12, #1
BNE WordCopyPLD1
SUBS r2, r2, #0x40
BNE WordCopyPLD
同樣的tips:這里每輪預(yù)取取多了议泵!
策略5: 多加載+預(yù)取
這里的優(yōu)勢(shì)是。桃熄。先口。如題~
LDMCopyPLD
PUSH {r4-r10}
LDMloopPLD
PLD [r1, #0x80]
LDMIA r1!, {r3 - r10}
STMIA r0!, {r3 - r10}
LDMIA r1!, {r3 - r10}
STMIA r0!, {r3 - r10}
SUBS r2, r2, #0x40
BGE LDMloopPLD
POP {r4-r10}
策略6: NEON + PLD
- 預(yù)取192byte;
- d0~d7共計(jì)8x64bit=64byte
這里計(jì)算剛剛好瞳收,都很完美自洽碉京!
NEONCopyPLD
PLD [r1, #0xC0]
VLDM r1!,{d0-d7}
VSTM r0!,{d0-d7}
SUBS r2,r2,#0x40
BGE NEONCopyPLD
策略7: Mixed ARM and NEON memory copy with preload
也就是說把各種指令穿插在一起組合處一個(gè)“多元體”來試驗(yàn)看看是不是會(huì)速度更快咯~
ARMNEONPLD
PUSH {r4-r11}
MOV r3, r0
ARMNEON
PLD [r1, #192]
PLD [r1, #256]
VLD1.64 {d0-d3}, [r1@128]!
VLD1.64 {d4-d7}, [r1@128]!
VLD1.64 {d16-d19}, [r1@128]!
LDM r1!, {r4-r11}
SUBS r2, r2, #128
VST1.64 {d0-d3}, [r3@128]!
VST1.64 {d4-d7}, [r3@128]!
VST1.64 {d16-d19}, [r3@128]!
STM r3!, {r4-r11}
BGT ARMNEON
POP {r4-r11}
測(cè)試時(shí)間結(jié)果
測(cè)試算法 | 時(shí)間花銷(ms) | 加速比 |
---|---|---|
傻搬 | 104.8 | 100% |
多加載(指令) | 94.5 | 111% |
NEON搬 | 104.8 | 100%(說明等待時(shí)間占主要比例) |
傻搬+預(yù)取 | 137.5 | 76% |
多加載+預(yù)取 | 106.6 | 98% |
NEON+預(yù)取 | 70.2 | 149% |
指令大雜燴 | 93.5 | 112% |
小結(jié):有一些奇怪的結(jié)論。
多加載指令僅僅提升了11%的性能螟深,但是我們沒有那么多指令了啊同時(shí)指令少就代表分枝預(yù)測(cè)里面的分支較少靶持妗!原因是:
- 指令cache100%擊中界弧,因此取指令是無須等待的凡蜻;
- 分枝預(yù)測(cè)在這里也不需要預(yù)測(cè)傻按钭邸!
- 單個(gè)寫(一個(gè)接一個(gè)地寫)划栓,memory system也把它當(dāng)成突發(fā)寫了兑巾,所以說效率并沒有顯著提升;
NEON指令居然沒有提升讀寫速度:
- 讀寫循環(huán)的執(zhí)行使用的寄存器很少忠荞,因此存在寄存器數(shù)據(jù)沖突的可能性就薪琛;
- 因?yàn)榧拇嫫饔玫纳僮耆鳎虼颂貏e適合搬小數(shù)據(jù)塊奋姿,因?yàn)槲覀儾恍枰褩2僮鱽砘謴?fù)現(xiàn)場(chǎng)啊K乇辍3剖!
- Cortex-A8處理器可以配置NEON加載數(shù)據(jù)的時(shí)候加載到L2 cache头遭;可以防止內(nèi)存copy過程中把L1中的不用數(shù)據(jù)給替換掉寓免;(?计维?袜香??)
盡管上面bb了一大堆好處鲫惶,但是實(shí)踐證明效果不咋地蜈首!
(這里存疑,我在A53平臺(tái)試驗(yàn)過欠母,大概會(huì)塊三倍的樣子盎恫摺!除非時(shí)間是異或操作的時(shí)間I吞省踩寇?)
PLD可以使得內(nèi)存控制器在數(shù)據(jù)被使用前就取到;因此加上NEON如虎添翼六水。
- 其次俺孙,我們知道在burst傳輸?shù)臅r(shí)候,第一次接入的延時(shí)是很大的掷贾,因此我們可以發(fā)起多次地請(qǐng)求到控制器(當(dāng)然得控制器夠先進(jìn)夠高級(jí))睛榄,這樣子控制器就會(huì)把后面的請(qǐng)求合到一起,從而把后面每一次的請(qǐng)求的接入延時(shí)相當(dāng)于去掉了想帅,第一個(gè)access latency均攤給每個(gè)request之后也忽略不計(jì)了懈费,這樣子好高效啊博脑!
影響內(nèi)存拷貝速度的因素
因素1: 要拷貝的數(shù)據(jù)量
有些實(shí)現(xiàn)需要一定的準(zhǔn)備時(shí)間憎乙,然后搬起來了就老快了票罐。
因此搬運(yùn)大數(shù)據(jù)塊的時(shí)候,可以把建立準(zhǔn)備的時(shí)間均攤泞边,因此還好该押,但是搬運(yùn)小塊數(shù)據(jù)的時(shí)候就不劃算了喲!
比如:在函數(shù)的開始stacking許多的寄存器阵谚,然后在主循環(huán)里面使用LDM跟STM指令來操作多個(gè)寄存器蚕礼;
因素2: 對(duì)齊Alignment
ARM架構(gòu)搬運(yùn)word對(duì)齊的數(shù)據(jù)會(huì)更高效;
courser alignment granularities這玩意能支撐性能梢什;
多加載指令在Cortex-A8 能每個(gè)周期從L1 cache加載2個(gè)寄存器的值奠蹬,但是只有地址是64位對(duì)齊的時(shí)候才可以。(所以NEON雖然一次可以搬運(yùn)128bit嗡午,但是你的CPU 位寬囤躁,DRAM位寬都是32bit的,因此數(shù)據(jù)最終還是被拆分成一個(gè)一個(gè)的32bit再存儲(chǔ)的荔睹,而我們的程序確實(shí)加速了是因?yàn)楫惢虿僮髂遣糠謺r(shí)間加速了袄暄荨!)
cache對(duì)齊也是有影響的捌宵距!
Cache behaviour (discussed later) can affect the performance of data accesses depending on its alignment relative to the size of a cache line. For the Cortex-A8, a level 1 cache line is 64 bytes, and a level 2 cache line is 64 bytes.
因素3: Memory特性
這里討論的操作(見上述諸程序)其性能瓶頸在存儲(chǔ)的接口部分。
因?yàn)槲覀兊难h(huán)很小啊吨拗,所以指令cache的就很好了满哪,而且并沒有計(jì)算部分(注意了,我們March C-代碼部分優(yōu)化是有數(shù)學(xué)運(yùn)算的劝篷,因此這部分比較費(fèi)時(shí))哨鸭,因此處理器的邏輯計(jì)算部分壓力并不大,因此速度因素極大地落在存儲(chǔ)器的速度上了携龟!
特定種類的memory在某種特定的讀寫模式下會(huì)性能更優(yōu)兔跌,比如SDRAM的burst傳輸需要一個(gè)很長(zhǎng)的延時(shí)來完成初始化操作勘高,但是一旦操作完成就能很快地完成后續(xù)的讀寫操作峡蟋;(我的MARCH算法加速部分把burst傳輸模式考慮進(jìn)去)
此外一個(gè)好的memory控制器是能夠并行接受很多讀寫請(qǐng)求的,并把這個(gè)initial latency給均攤掉华望。
此外一些特定的代碼讀寫順序也可能改善性能蕊蝗;
因素4: cache的使用
大量數(shù)據(jù)搬運(yùn)的時(shí)候,很顯然會(huì)把cache里面的數(shù)據(jù)全部“換血”的赖舟;
盡管這在內(nèi)存自身拷貝的時(shí)候不會(huì)有什么影響蓬戚,但是它可能會(huì)減速后面的代碼,最終降低整體的性能宾抓。
因素5: Code dependencies
在標(biāo)準(zhǔn)的 memcpy()函數(shù)運(yùn)行時(shí)子漩,尤其遇上慢速的memory時(shí)豫喧,處理器大部分時(shí)間都沒有被使用。
因此我們可以考慮在memcopy期間運(yùn)行一些其他的代碼幢泼;
因?yàn)閙emcpy()時(shí)阻塞的紧显,因此只有函數(shù)結(jié)束才會(huì)返回,而此時(shí)cpu時(shí)被占死了缕棵;
我們可以使用管道來實(shí)現(xiàn)孵班,把memcpy()放倒后臺(tái)運(yùn)行,然后通過poll或者中斷來隨時(shí)監(jiān)控內(nèi)存搬運(yùn)的情況
使用DMA操作招驴,這樣完全解放CPU了篙程;并把數(shù)據(jù)塊打碎這樣就能一邊搬運(yùn)一遍操作了!效率提高了呢1鹄濉(都是一些很常規(guī)的想法呢J觥)
cortex-A8內(nèi)置的預(yù)加載引擎
數(shù)據(jù)預(yù)加載到L2 cache;
我們CPU先啟動(dòng)預(yù)加載指令丹允,然后就去干別的活郭厌,直到接到電話(中斷)說加載完成了,那么我就可以去對(duì)它進(jìn)行操作了雕蔽,操作完之后繼續(xù)下一輪折柠;
- 使用其他處理器
內(nèi)嵌的其他核原理同DMA;
附錄 PLD基本思想及應(yīng)用考量
當(dāng)我們?cè)诎沧科脚_(tái)需要處理圖像數(shù)據(jù)的時(shí)候批狐,其中一個(gè)基本的操作就是把大量的數(shù)據(jù)從內(nèi)存搬來搬去扇售,相對(duì)于CPU來說這個(gè)很耗時(shí)間,除了NEON加速之外嚣艇,上面提到的PLD加速也是很有效的承冰,具體多有效我們看實(shí)例。
我們比如在處理攝像頭數(shù)據(jù)的時(shí)候食零,內(nèi)存中搬數(shù)據(jù)一般是這樣寫的:
while (n--) {
*dest++ = *src++;
}
在我當(dāng)前平臺(tái)上跑了一下困乒,1MB的空間搬完需要25ms的樣子,這個(gè)就很費(fèi)時(shí)了贰谣,究其根本娜搂,是因?yàn)閿?shù)據(jù)不在處理器的cache中,因此CPU需要花時(shí)間來等你DRAM傳過來吱抚,也就是說捐康,是我當(dāng)前的CPU帶寬大于存儲(chǔ)器的帶寬了忠聚。
因此解決方案就是我們提前把數(shù)據(jù)放到cache中,由于cache帶寬大于DRAM小于CPU,因此可以減少等待時(shí)間敦锌。
改進(jìn)后的代碼如下所示,提前預(yù)取數(shù)據(jù),也就是在真正搬運(yùn)數(shù)據(jù)之前目標(biāo)數(shù)據(jù)就被放倒cache中來了,等真正搬數(shù)據(jù)時(shí)涮坐,一下子就在cache中hit了,馬上走你誓军!效率快很多膊升!
這里有個(gè)小細(xì)節(jié),每次只搬運(yùn)了32bit也就是4byte谭企,但是我們預(yù)取了128byte廓译,這樣子豈不是cache很快就溢出了?關(guān)于溢出CPU會(huì)怎么處理债查,我就沒深究了非区,這也不是這段代碼的主旨,權(quán)當(dāng)留下我自己的一個(gè)思考吧盹廷!
這個(gè)酒厲害了征绸,時(shí)間變?yōu)?ms了,提速了三倍之多俄占!
while (n--) {
asm ("PLD [%0, #128]"::"r" (src));
*dest++ = *src++;
}
當(dāng)然了管怠,優(yōu)化這種事情時(shí)做不完的,你可以隨著環(huán)境的變換不斷優(yōu)化的:
- 比如這里缸榄,我們預(yù)取后存在溢出問題渤弛,也就是說沒有物盡其用;
- 其次甚带,循環(huán)里面就一句搬運(yùn)指令她肯,然后就是n--操作,判斷操作等鹰贵,這樣子來看的話一個(gè)loop里面真正干活的指令占的比例很小晴氨,因此也是一種浪費(fèi),把資源浪費(fèi)在一些不重要的事情上了碉输!
算法里面又個(gè)概念籽前,就是時(shí)間跟空間是可以互相轉(zhuǎn)換的,在這里的表現(xiàn)就是我通過多寫一些代碼操作敷钾,從而將有效操作時(shí)間在每個(gè)loop中的比例提升上來枝哄。
如下所示,我每個(gè)loop里面增加3個(gè)搬運(yùn)操作闰非,這樣就保證了cache不會(huì)溢出膘格,同時(shí)每個(gè)loop中數(shù)據(jù)搬運(yùn)部分所占的CPU時(shí)間比例提升了峭范,也即“有效功率”提升了财松!
這樣子做大概又加快了1ms的樣子!
n /= 4; //assume it's multiple of 4
while (n--) {
asm ("PLD [%0, #128]"::"r" (src));
*dest++ = *src++;
*dest++ = *src++;
*dest++ = *src++;
*dest++ = *src++;
}
思路總結(jié)
這個(gè)世界是由基本的元素組成的,操作系統(tǒng)是由一些基本的門電路架起來的辆毡,雪花是分形的菜秦,因此我們總能在深入一個(gè)案例后,發(fā)現(xiàn)一些普世的道理舶掖!共勉球昨!
我說過:“優(yōu)化時(shí)可以隨著環(huán)境的變化一直做的!”(名言罢H痢主慰!記住啊鲫售!劃重點(diǎn)共螺!要考的!)
這里不是有四個(gè)搬運(yùn)操作嘛情竹!編譯器優(yōu)化后也不知道會(huì)優(yōu)化成啥樣子藐不,我們可以直接用匯編嘛!匯編里面的四個(gè)LDR/STR指令秦效,然后是LDRM/STRM可以節(jié)省三次CPU指令操作時(shí)間及等待時(shí)間雏蛮,然后就是NEON的并行加速了!哎呀呀~不得了阱州!只會(huì)越來愈快挑秉!具體多快!你自己去實(shí)驗(yàn)咯苔货!