怎么進(jìn)行Cache的性能分析

這篇文章給大家介紹怎么進(jìn)行Cache的性能分析,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

創(chuàng)新互聯(lián)公司專注于企業(yè)成都全網(wǎng)營銷、網(wǎng)站重做改版、天河網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、HTML5建站、商城網(wǎng)站開發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)營銷網(wǎng)站建設(shè)、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為天河等各大城市提供網(wǎng)站開發(fā)制作服務(wù)。

Lua5.4-alpha-rc2 已經(jīng)發(fā)布了好一段時(shí)間了, 一直沒時(shí)間去跑跑看性能如何。最近剛好有空,就跑來看看。結(jié)果第一段測(cè)試代碼就把我驚住了。

--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 上,這段代碼運(yùn)行時(shí)間分為0.55,0.42s。

通過`./luac -p -l ./lua ` 可以得知,上段這代碼性能熱點(diǎn)一定是OP_MOVE,和OP_FORLOOP。因此一定是這兩個(gè)opcode的執(zhí)行解釋代碼有修改。

我仔細(xì)對(duì)比了一下,關(guān)于OP_FORLOOP和OP_MOVE的實(shí)現(xiàn),發(fā)現(xiàn)實(shí)現(xiàn)上一共有三處優(yōu)化。

1. vmcase(OP_FORLOOP)的執(zhí)行代碼去掉了’0<step’的判斷。(由于一次for循環(huán)期間,step的符號(hào)總是固定的,因此cpu分支預(yù)測(cè)成功率是100%)

2. vmcase(OP_FORLOOP)向回跳轉(zhuǎn)時(shí),偏移量改成了正值,因此將Bx寄存器直接當(dāng)作無符號(hào)數(shù)去處理,省了一個(gè)符號(hào)轉(zhuǎn)換操作。

3. vmcase(OP_FORLOOP)向回跳轉(zhuǎn)時(shí),由直接修改ci->u.savedpc改為了修改一個(gè)局部變量pc。通過反匯編得知,修改局部pc可以省掉一次store操作。

經(jīng)過測(cè)試發(fā)現(xiàn),這三處修改都達(dá)不到0.13s這么大幅度的提升。

萬般無奈的情況下,我使用git bisec測(cè)試了從 Lua5.3.4 到 Lua5.4-alpha-rc2的所有變更(這里說所有不準(zhǔn)確,因?yàn)間it bisec是通過二分法查找的)。

最終發(fā)現(xiàn)引起性能影響的竟然是下面一段賦值操作的修改。

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); }

兩個(gè)賦值的作用都是復(fù)制一個(gè)結(jié)構(gòu)體。只不過由于結(jié)構(gòu)體對(duì)齊的存在,直接使用結(jié)構(gòu)體賦值,會(huì)多復(fù)制了四個(gè)字節(jié)。

但是,在64bit機(jī)器上,如果地址是對(duì)齊的,復(fù)制4個(gè)字節(jié)和復(fù)制8個(gè)字節(jié)不應(yīng)該會(huì)有如此大的差異才對(duì)。畢竟都是一條指令完成的。為了近一步證明不是多復(fù)制4個(gè)字節(jié)帶來的開銷,我做了如下測(cè)試。

假設(shè)修改前的setobj是setobj_X, 修改后的setobj為setobj_Y。然后分別對(duì)setobj_X和setobj_Y進(jìn)行測(cè)試tt_類型為char, short, int, long的情況。

測(cè)試結(jié)果如下:

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

從測(cè)試結(jié)果可以看到setobj_X在tt_類型為long是反而是最快的,這就說明開銷并不是多復(fù)制4字節(jié)造成的。

反匯編之后發(fā)現(xiàn),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

猜測(cè),難道是賦值順序打亂了流水線并行,還是尋址模式需要額外的機(jī)器周期? 但是他們都無法解釋,當(dāng)我把tt_的類型改為long之后,setobj_X也會(huì)變得更快。

種種跡象把矛頭指向Cache。 但這時(shí)我已經(jīng)黔驢技窮了,我找不到更多的測(cè)試來繼續(xù)縮小排查范圍了。也沒有辦法進(jìn)一步確定一定是Cache造成的(我這時(shí)還不知道PMU的存在)。

我開始查找《64-ia-32-architectures-optimization-manual》,試圖能在這里找到答案。

找來找去,只在3.6.5.1節(jié)中找到了關(guān)于L1D Cache效率的相關(guān)的內(nèi)容。我又仔細(xì)閱讀了一下lvm.c的代碼,卻并沒有發(fā)現(xiàn)符合產(chǎn)生 Cache 懲罰的條件。(其實(shí)這里我犯了一個(gè)錯(cuò)誤,不然這里我就已經(jīng)找到答案了。以前看lparse.c中關(guān)于OP_FORLOOP部分時(shí)不仔細(xì)。欠的技術(shù)債這里終于還了。)

萬般無奈下,我又測(cè)試了下面代碼,想看看能否進(jìn)一步縮小推斷范圍。

--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()

這次測(cè)試其實(shí)是有點(diǎn)意外的,因?yàn)閟etobj_X版本的luaVM一下子跑的幾乎跟setobj_Y版本一樣快了。

看起來更像是3.6.5.1節(jié)中提到的L1D Cache的懲罰問題了。但是我依然沒有找到懲罰的原因。

我把這一測(cè)試結(jié)果同步到lua的maillist上去(在我反匯編找不到答案后,就已經(jīng)去maillist上提問了,雖然有進(jìn)度,但是同樣一直沒有結(jié)論).

這一次maillist上的同學(xué),終于有了進(jìn)一步答案了。

他指出,在vmcase(OP_FORLOOP)中使用分開賦值的方式更新’i’(一次賦值value_, 一次賦值tt_,這次tt_賦值是store 32bit)。而在vmcase(OP_MOVE)使用的setobj_X賦值時(shí),使用了兩次load 64位來讀取value_和tt_。

這恰好就是3.6.5.1節(jié)中提到的規(guī)則(b),因此會(huì)有L1D Cache懲罰。

而這時(shí)我恰好已經(jīng)通過perf觀察到兩個(gè)版本的setobj在PMU的l1d_pend_miss.pending_cycles和l1d_pend_miss.pending_cycles_any指標(biāo)上有顯著不同。 兩相印證,基本可以90%的肯定就是這個(gè)問題。

現(xiàn)在來解釋一下,我之前犯的錯(cuò)誤。我之前一直認(rèn)為,一個(gè)`for i = 1, 3, 1 do end`一共占三個(gè)lua寄存器:一個(gè)初始值i,一個(gè)最大值3, 暫時(shí)稱為_m,一個(gè)步長(zhǎng)1, 暫時(shí)稱為_s。

但是經(jīng)過maillist上的同學(xué)提醒后,我又仔細(xì)看了一下lparse.c,發(fā)現(xiàn)其實(shí)上面的for一共占四個(gè)lua寄存器:初始值1,暫稱為_i,最大值_m, 步長(zhǎng)_s,及變量i。

每次OP_FORLOOP在執(zhí)行到最后會(huì)同步_i的值到變量i. 代碼中的使用的值來自變量i所在的寄存器,而不是_i。

從lparse.c中得知,_i來自R(A), _m來自R(A+1), _s來自R(A+2), i來自R(A+3)。

再來看一下lvm.c中關(guān)于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是分別對(duì)value_和tt_進(jìn)行賦值。

因此當(dāng)接下來執(zhí)行vmcase(OP_MOVE)時(shí),setobj_X對(duì)tt_所在的地址,直接讀取64位時(shí)就就會(huì)受到L1D Cache的懲罰。

而我之前犯的錯(cuò)誤就是我一直認(rèn)為修改i的值是通過chgivalue(ra, idx)來實(shí)現(xiàn)的。

為了更加確定是L1D Cache中Store-to-Load-Forwarding懲罰造成的開銷。我將setivalue改為了chgivalue之后再測(cè)試。果然運(yùn)行時(shí)間與setobj_Y的時(shí)間相差無幾。這下結(jié)論已經(jīng)99%可靠了,那剩下的1%恐怕要問Intel工程師了。

關(guān)于怎么進(jìn)行Cache的性能分析就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

本文名稱:怎么進(jìn)行Cache的性能分析
URL鏈接:http://www.muchs.cn/article2/jpcsic.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供面包屑導(dǎo)航品牌網(wǎng)站制作、外貿(mào)建站網(wǎng)頁設(shè)計(jì)公司、做網(wǎng)站、移動(dòng)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

h5響應(yīng)式網(wǎng)站建設(shè)