您好,登錄后才能下訂單哦!
這篇文章給大家介紹怎么進行Cache的性能分析,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
Lua5.4-alpha-rc2 已經發布了好一段時間了, 一直沒時間去跑跑看性能如何。最近剛好有空,就跑來看看。結果第一段測試代碼就把我驚住了。
--a.lua
collectgarbage("stop")
local function foo()
local a = 3
for i = 1, 64 * 1024 * 1024 do
a = i
end
print(a)
end
foo()
在 Lua5.3.4 和 Lua5.4-alpha-rc2 上,這段代碼運行時間分為0.55,0.42s。
通過`./luac -p -l ./lua ` 可以得知,上段這代碼性能熱點一定是OP_MOVE,和OP_FORLOOP。因此一定是這兩個opcode的執行解釋代碼有修改。
我仔細對比了一下,關于OP_FORLOOP和OP_MOVE的實現,發現實現上一共有三處優化。
1. vmcase(OP_FORLOOP)的執行代碼去掉了’0<step’的判斷。(由于一次for循環期間,step的符號總是固定的,因此cpu分支預測成功率是100%)
2. vmcase(OP_FORLOOP)向回跳轉時,偏移量改成了正值,因此將Bx寄存器直接當作無符號數去處理,省了一個符號轉換操作。
3. vmcase(OP_FORLOOP)向回跳轉時,由直接修改ci->u.savedpc改為了修改一個局部變量pc。通過反匯編得知,修改局部pc可以省掉一次store操作。
經過測試發現,這三處修改都達不到0.13s這么大幅度的提升。
萬般無奈的情況下,我使用git bisec測試了從 Lua5.3.4 到 Lua5.4-alpha-rc2的所有變更(這里說所有不準確,因為git bisec是通過二分法查找的)。
最終發現引起性能影響的竟然是下面一段賦值操作的修改。
typedef union Value {
GCObject *gc; /* collectable objects */
void *p; /* light userdata */
int b; /* booleans */
lua_CFunction f; /* light C functions */
lua_Integer i; /* integer numbers */
lua_Number n; /* float numbers */
} Value;
#define TValuefields Value value_; int tt_
typedef struct lua_TValue {
TValuefields;
} TValue;
#define setobj(L,obj1,obj2) \
-{ TValue *io1=(obj1); *io1 = *(obj2); \
+{ TValue *io1=(obj1); const TValue *io2=(obj2); \
+ io1->value_ = io2->value_; io1->tt_ = io2->tt_; \
(void)L; checkliveness(L,io1); }
兩個賦值的作用都是復制一個結構體。只不過由于結構體對齊的存在,直接使用結構體賦值,會多復制了四個字節。
但是,在64bit機器上,如果地址是對齊的,復制4個字節和復制8個字節不應該會有如此大的差異才對。畢竟都是一條指令完成的。為了近一步證明不是多復制4個字節帶來的開銷,我做了如下測試。
假設修改前的setobj是setobj_X, 修改后的setobj為setobj_Y。然后分別對setobj_X和setobj_Y進行測試tt_類型為char, short, int, long的情況。
測試結果如下:
typeof(tt_) char short int long
setobj_X 0.55s 0.55s 0.55s 0.41s
setobj_Y 0.52s 0.43s 0.42s 0.42s
從測試結果可以看到setobj_X在tt_類型為long是反而是最快的,這就說明開銷并不是多復制4字節造成的。
反匯編之后發現,setobj_X 和 setobj_Y 惟一的差別就是賦值順序和尋址模式。
匯編如下:
;setobj_X
0x413e10: shr r13d,0x17
0x413e14 : shl r13,0x4
0x413e18 : mov rax,QWORD PTR [r15+r13*1] ;value_
0x413e1c : mov rdx,QWORD PTR [r15+r13*1+0x8] ;tt_
0x413e21 : mov QWORD PTR [rbx],rax
0x413e24 : mov QWORD PTR [rbx+0x8],rdx
0x413e28 : mov rsi,QWORD PTR [rbp+0x28]
0x413e2c : jmp 0x4131a0
;setobj_Y
0x413da8: shr r13d,0x17
0x413dac : shl r13,0x4
0x413db0 : add r13,r15
0x413db3 : mov eax,DWORD PTR [r13+0x8] ;tt_
0x413db7 : mov DWORD PTR [rbx+0x8],eax
0x413dba : mov rax,QWORD PTR [r13+0x0] ;value_
0x413dbe : mov QWORD PTR [rbx],rax
0x413dc1 : mov rax,QWORD PTR [rbp+0x28]
0x413dc5 : jmp 0x413170
猜測,難道是賦值順序打亂了流水線并行,還是尋址模式需要額外的機器周期? 但是他們都無法解釋,當我把tt_的類型改為long之后,setobj_X也會變得更快。
種種跡象把矛頭指向Cache。 但這時我已經黔驢技窮了,我找不到更多的測試來繼續縮小排查范圍了。也沒有辦法進一步確定一定是Cache造成的(我這時還不知道PMU的存在)。
我開始查找《64-ia-32-architectures-optimization-manual》,試圖能在這里找到答案。
找來找去,只在3.6.5.1節中找到了關于L1D Cache效率的相關的內容。我又仔細閱讀了一下lvm.c的代碼,卻并沒有發現符合產生 Cache 懲罰的條件。(其實這里我犯了一個錯誤,不然這里我就已經找到答案了。以前看lparse.c中關于OP_FORLOOP部分時不仔細。欠的技術債這里終于還了。)
萬般無奈下,我又測試了下面代碼,想看看能否進一步縮小推斷范圍。
--b.lua
collectgarbage("stop")
local function foo()
local a = 3
local b = 4
for i = 1, 64 * 1024 * 1024 do
a = b
end
print(a)
end
foo()
這次測試其實是有點意外的,因為setobj_X版本的luaVM一下子跑的幾乎跟setobj_Y版本一樣快了。
看起來更像是3.6.5.1節中提到的L1D Cache的懲罰問題了。但是我依然沒有找到懲罰的原因。
我把這一測試結果同步到lua的maillist上去(在我反匯編找不到答案后,就已經去maillist上提問了,雖然有進度,但是同樣一直沒有結論).
這一次maillist上的同學,終于有了進一步答案了。
他指出,在vmcase(OP_FORLOOP)中使用分開賦值的方式更新’i’(一次賦值value_, 一次賦值tt_,這次tt_賦值是store 32bit)。而在vmcase(OP_MOVE)使用的setobj_X賦值時,使用了兩次load 64位來讀取value_和tt_。
這恰好就是3.6.5.1節中提到的規則(b),因此會有L1D Cache懲罰。
而這時我恰好已經通過perf觀察到兩個版本的setobj在PMU的l1d_pend_miss.pending_cycles和l1d_pend_miss.pending_cycles_any指標上有顯著不同。 兩相印證,基本可以90%的肯定就是這個問題。
現在來解釋一下,我之前犯的錯誤。我之前一直認為,一個`for i = 1, 3, 1 do end`一共占三個lua寄存器:一個初始值i,一個最大值3, 暫時稱為_m,一個步長1, 暫時稱為_s。
但是經過maillist上的同學提醒后,我又仔細看了一下lparse.c,發現其實上面的for一共占四個lua寄存器:初始值1,暫稱為_i,最大值_m, 步長_s,及變量i。
每次OP_FORLOOP在執行到最后會同步_i的值到變量i. 代碼中的使用的值來自變量i所在的寄存器,而不是_i。
從lparse.c中得知,_i來自R(A), _m來自R(A+1), _s來自R(A+2), i來自R(A+3)。
再來看一下lvm.c中關于vmcase(OP_FORLOOP)的代碼:
vmcase(OP_FORLOOP) {
if (ttisinteger(ra)) { /* integer loop? */
lua_Integer step = ivalue(ra + 2);
lua_Integer idx = intop(+, ivalue(ra), step);
lua_Integer limit = ivalue(ra + 1);
if ((0 < step) ? (idx <= limit) : (limit <= idx)) {
ci->u.l.savedpc += GETARG_sBx(i); /* jump back */
chgivalue(ra, idx); /* update internal index... */
setivalue(ra + 3, idx); /* ...and external index */
}
}
...
vmbreak;
}
可以很明顯看出ra寄存器和(ra+3)的寄存器的賦值方式并不一樣。其中chgivalue是只改value_部分,而setivalue是分別對value_和tt_進行賦值。
因此當接下來執行vmcase(OP_MOVE)時,setobj_X對tt_所在的地址,直接讀取64位時就就會受到L1D Cache的懲罰。
而我之前犯的錯誤就是我一直認為修改i的值是通過chgivalue(ra, idx)來實現的。
為了更加確定是L1D Cache中Store-to-Load-Forwarding懲罰造成的開銷。我將setivalue改為了chgivalue之后再測試。果然運行時間與setobj_Y的時間相差無幾。這下結論已經99%可靠了,那剩下的1%恐怕要問Intel工程師了。
關于怎么進行Cache的性能分析就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。