【轉(zhuǎn)】你們的M1還好用嗎?蘋果的黑魔法?Apple M1的棧操作消除
?你們的M1還好用嗎?

JamesAslan
喜歡畫畫和攝影的硅工碼農(nóng)(滑稽)

你關(guān)注的 Luv Letter 贊同
前言
訪存是微結(jié)構(gòu)設(shè)計(jì)永恒的難題,這個(gè)領(lǐng)域也有著一個(gè)傳說:部分處理器能夠?qū)2僮飨RM架構(gòu)并沒有專門的棧操作指令,那么應(yīng)該如何定義棧操作呢?我們不妨將其寬泛定義為:訪問同一物理地址的store和load指令對(duì)(后文中將用棧操作來指代此類情況),棧操作無疑被包含其中。眾所周知,蘋果M1處理器有著極高的IPC,采用了許多激進(jìn)的設(shè)計(jì)來提高指令并行度,那么它能不能消除棧操作呢?讓我們來探究一番。
一點(diǎn)小小的劇透:其實(shí)最新的x86陣營(yíng)處理器在這一方面遠(yuǎn)比蘋果激進(jìn),它們的故事我們以后再講;而蘋果的實(shí)現(xiàn)方式與它們并不相同,這有可能是ARM ISA本身的特性或是實(shí)際需求使然。本文探究了Apple M1棧操作消除的實(shí)現(xiàn)方式,并揭示了其ICache以及流水線設(shè)計(jì)的部分細(xì)節(jié):
JamesAslan:蘋果的黑魔法?(下)Apple M1的棧操作消除98 贊同 · 8 評(píng)論文章

一點(diǎn)題外話:在上一篇文章中我們探究了13900ES預(yù)取器的反常行為:
JamesAslan:這L2 Cache有點(diǎn)怪,13900ES的BUG還是FEATURE?169 贊同 · 11 評(píng)論文章

后來翻閱Intel官方PPT時(shí)發(fā)現(xiàn):

看來有可能是一個(gè)FEATURE,能夠見證行業(yè)巨頭在面對(duì)取舍時(shí)的小小窘迫,還是頗有趣味的。
正文
測(cè)試構(gòu)造
首先,我們需要構(gòu)造一個(gè)Probe用的小程序來反應(yīng)處理器執(zhí)行棧操作(訪問同一物理地址的store和load指令對(duì),后文中將用棧操作來指代此類情況)的性能。從定義入手其實(shí)基本結(jié)構(gòu)十分簡(jiǎn)單,只要一條store指令和一條load指令(將其稱為一個(gè)指令塊)不斷循環(huán)往復(fù)即可,使用匯編構(gòu)造如下:
str x11, [x10,#0] // x11為store的(值的)源寄存器;[x10,#0]為store的地址,含義為:地址x10+0ldr x12, [x10,#0] // x12為load的目的寄存器;[x10,#0]為load的地址,含義為:地址x10+0
可以看到,這樣的棧操作其實(shí)可以被迅速完成而不需要通過Cache,因?yàn)閘oad的值顯然就是store存入的值;倘若處理器在流水線內(nèi)就獲得了load指令的值,我們就將這一操作稱為棧操作消除(沒有通過Cache獲得結(jié)果)。
注意ARM匯編中即便同為ldr,也有包括post index在內(nèi)的多種尋址方式,需要使用正確的匯編。但是很快我們就會(huì)發(fā)現(xiàn)問題:
str x11, [x10,#0]ldr x12, [x10,#0]------------------str x11, [x10,#0]ldr x12, [x10,#0]------------------str x11, [x10,#0]ldr x12, [x10,#0]
循環(huán)執(zhí)行這樣的多個(gè)指令塊,指令塊之間并沒有任何依賴,因此塊間可以完美并行,這樣是無法反應(yīng)棧操作的性能的。我們不妨將store指令與load指令的值寄存器設(shè)置為同一個(gè):
str x11, [x10,#0]ldr x11, [x10,#0]------------------str x11, [x10,#0]ldr x11, [x10,#0]------------------str x11, [x10,#0]ldr x11, [x10,#0]
此時(shí)上一個(gè)塊的load指令的結(jié)果會(huì)作為下一個(gè)塊的store指令的值來源,因此倘若上一個(gè)塊的load指令無法取回?cái)?shù)據(jù),下一個(gè)塊的store指令就無法完成;整個(gè)程序通過訪存地址相關(guān)和寄存器相關(guān)串成了一條巨大的相關(guān)鏈,其執(zhí)行時(shí)長(zhǎng)即可反應(yīng)我們需要的棧操作性能。此類測(cè)試的其他構(gòu)建細(xì)節(jié)在專欄的其他文章內(nèi)有詳細(xì)講解,簡(jiǎn)而言之,整個(gè)程序分為外循環(huán)與內(nèi)循環(huán),內(nèi)循環(huán)為N個(gè)指令塊(不僅僅使用1個(gè)指令塊,否則循環(huán)相關(guān)的指令占比過大,它們不是我們計(jì)時(shí)的對(duì)象,需要通過通過放置N個(gè)指令塊稀釋),外循環(huán)為M次(多次執(zhí)行,使得執(zhí)行時(shí)長(zhǎng)足夠長(zhǎng),方便計(jì)時(shí)),則共會(huì)執(zhí)行N*M次指令塊;我們將總執(zhí)行時(shí)長(zhǎng)除以N*M,再通過頻率信息就可以得出每個(gè)指令塊執(zhí)行所需的cycle數(shù),以下均以cycle數(shù)標(biāo)識(shí)棧操作性能。
//測(cè)試程序示意,實(shí)際程序?yàn)榍度雲(yún)R編:for(int i=0;i<M;i++){
? ?//共N組store和load ? ?str x11, [x10,#0]
? ?ldr x11, [x10,#0]
? ?...........
? ?str x11, [x10,#0]
? ?ldr x11, [x10,#0]}
基準(zhǔn)獲取
現(xiàn)在有請(qǐng)本次測(cè)試的背景板:ARM Cortex X1,其作為ARM公版的首個(gè)超大核微架構(gòu),承載了ARM沖擊高性能市場(chǎng)的愿景,不妨看看其表現(xiàn)如何。為了排除取指帶寬波動(dòng)的影響,我們根據(jù)處理器的微結(jié)構(gòu)信息,在指令塊中插入指定數(shù)量的nop指令,使得一個(gè)cycle理論上只能取指、重命名一個(gè)指令塊。對(duì)于Cortex X1這樣的6發(fā)射處理器,調(diào)整指令塊如下使其正好容納6條指令:
str x11, [x10,#0]nopnopnopnopldr x11, [x10,#0]

由于我們的主要探究對(duì)象并不是Cortex X1,其非整結(jié)果我們不做深入探究,不妨將其視為4-5cycle。這樣的處理器顯然沒有進(jìn)行棧操作消除,符合我們對(duì)背景板處理器的性能預(yù)期,示意流水線時(shí)空?qǐng)D如下:

可見,在沒有棧操作優(yōu)化時(shí)由于寄存器相關(guān),執(zhí)行一個(gè)指令塊所需的時(shí)間約為load指令的load-to-use延遲(視具體流水線設(shè)計(jì)會(huì)有區(qū)別,影響因素較多,包括但不限于:1.有無store buffer前遞。2.store?order violation避免機(jī)制的設(shè)計(jì)。)
棧操作消除
既然ARM Cortex X1沒有棧操作消除,那么Apple M1呢?針對(duì)8發(fā)射的Firestorm,我們調(diào)整指令塊如下:
str x11, [x10,#0]nopnopnopnopnopnopldr x11, [x10,#0]

僅耗時(shí)約1.6 cycle!無疑M1擁有棧操作的優(yōu)化,不愧是ARM陣營(yíng)的扛把子選手!這樣的優(yōu)化簡(jiǎn)直可以稱之為消除了,倘若load被實(shí)際執(zhí)行,縱使有store buffer的數(shù)據(jù)前遞,其延遲也難以被壓縮至1 -2 cycle。

但是故事顯然不能僅此而已,不妨看看M1的棧操作消除面對(duì)更加復(fù)雜的情況表現(xiàn)如何。我們修改指令塊,在我們的棧操作store、load對(duì)之間加入一條不相關(guān)的store指令(操作了不同的地址):
str x11, [x10,#0]str x11, [x10,#8] //str指令操作位寬為64bit,即8字節(jié);偏移量為8時(shí)兩條str的地址不會(huì)有重疊nopnopnopnopnopldr x11, [x10,#0]

略有波動(dòng),但是棧操作消除仍然生效,看來M1不會(huì)被如此簡(jiǎn)單的手法欺騙。我們添加兩對(duì)相互嵌套的操作:
str x11, [x10,#0]str x12, [x10,#8]nopnopnopnopldr x12, [x10,#8]ldr x11, [x10,#0]

仍然是兩對(duì)棧操作,但是并不完美嵌套:
str x11, [x10,#0]str x12, [x10,#8]nopnopnopnopldr x11, [x10,#0]ldr x12, [x10,#8]

可以看到雖然消除效果逐漸變差,但是仍然存在明顯的棧操作消除現(xiàn)象。我們不再繼續(xù)增加一個(gè)指令塊中的棧操作對(duì)數(shù),因?yàn)镸1理論上每周期只能執(zhí)行2條load指令與2條store指令(其實(shí)即便增加到3對(duì),棧操作消除的效果仍然明顯存在,但是為了數(shù)據(jù)的美觀性不予展示)。
瑕疵初現(xiàn)
一個(gè)機(jī)制不可能面面俱到,我們肯定能夠通過精心構(gòu)造指令對(duì)使其失效;不過在此之前,我們還得請(qǐng)出老朋友:perf stat工具。如果要在M1平臺(tái)上使用perf工具,為其安裝Arch linux是較為方便的選擇;只要學(xué)會(huì)正確指定核心+事件號(hào),其體驗(yàn)與x86平臺(tái)基本無異。但是如果不想覆蓋原操作系統(tǒng),MacOS上也是可以使用性能計(jì)數(shù)器的,只是過程相對(duì)曲折;本次我在個(gè)人日用的Macbook air上使用MacOS進(jìn)行測(cè)試。蘋果顯然對(duì)于自己的性能計(jì)數(shù)器信息較為忸怩,我們首先要自己從系統(tǒng)目錄中獲取性能計(jì)數(shù)器的描述。通過一些手段,我們可以找到a14.plist、a15.plist兩個(gè)文件,M1使用的是與a14近乎相同的Firestorm,因此我們參考a14.plist中的信息。實(shí)際上這兩個(gè)文件內(nèi)容近乎一模一樣,可見兩代處理器的性能計(jì)數(shù)器部分并無明顯變動(dòng)(或者說開發(fā)者可以訪問的性能計(jì)數(shù)器部分并無明顯改動(dòng))。從中我們挑選出所需的計(jì)數(shù)器如下:
Event NameEvent DiscriptionFIXED_CYCLESFIXED_INSTRUCTIONSST_MEMORY_ORDER_VIOLATION_NONSPECRetired stores that triggered memory order violationsFLUSH_RESTART_OTHER_NONSPECPipeline flush and restarts that were not due to branch mispredictions or memory order violationsLD_UNIT_UOPUops that flowed through the Load UnitST_UNIT_UOPUops that flowed through the Store UnitMAP_STALLCycles while the Map Unit was stalled for any reasonMAP_STALL_DISPATCHCycles while the Map Unit was stalled because of Dispatch back pressure
M1中提供6個(gè)可配置性能計(jì)數(shù)器,足夠我們的使用了。接著,我們需要一些手段來訪問這些計(jì)數(shù)器。好消息是實(shí)際上MacOS提供了類似的接口,即Kperf動(dòng)態(tài)庫;壞消息是這個(gè)庫并不對(duì)外開放;好消息是前人通過逆向工程破解了其信息。然后,我們只需要調(diào)用Kperf即可讀取所需的計(jì)數(shù)器;這里使用前人大佬ibireme的框架,將我們的測(cè)試程序作為profile_func嵌入其中,運(yùn)行效果如下:

我們終于可以一窺處理器的運(yùn)行狀態(tài),可見store-order-violation predictor工作得很好,運(yùn)行過程中僅有極少的訪存序引起的回滾。重命名堵塞較多且均由發(fā)射隊(duì)列(對(duì)于Apple的設(shè)計(jì)而言,其實(shí)是第一級(jí)的分派隊(duì)列)的反壓造成;因?yàn)槲覀兊闹噶顗K平均需要1.6周期才能執(zhí)行完成,但是處理器前端每周期都能取回1個(gè)指令塊,因此大量的訪存指令堆積在分派隊(duì)列和發(fā)射隊(duì)列中,最終導(dǎo)致了重命名階段堵塞。一切都符合預(yù)期。
至此,我們終于可以給棧操作消除機(jī)制找點(diǎn)麻煩了。棧操作消除可以基于兩種較為簡(jiǎn)單的思路:
基于靜態(tài)信息,例如操作數(shù)的源寄存器號(hào)。倘若一對(duì)store與load指令用于計(jì)算地址的源寄存器和偏移都相同,那我們顯然可以推測(cè)store與load訪問了同一地址,并對(duì)它們進(jìn)行消除。消除的方式多種多樣,較為簡(jiǎn)單的即直接通過重命名機(jī)制,將load指令的目的寄存器重命名為store指令的值的源寄存器;但是為了確保這樣的消除是正確的,load指令仍然需要被執(zhí)行,以驗(yàn)證地址的正確性以及它們真的相等,因此只是消除了執(zhí)行延遲而并非整個(gè)指令的執(zhí)行。
基于歷史信息,例如指令PC。倘若一對(duì)store與load指令曾經(jīng)訪問過相同的物理地址,那我們將它們標(biāo)記為可能再次訪問同一地址,并對(duì)它們進(jìn)行消除。消除的方式可以與上一點(diǎn)近似,同樣其也需要被執(zhí)行來驗(yàn)證正確性。
在具體實(shí)現(xiàn)的過程中,無論是上述哪種方式都會(huì)遇到各種各樣的細(xì)節(jié)問題,例如如何記錄和存儲(chǔ)store指令的信息、使用邏輯寄存器號(hào)還是物理寄存器號(hào)等,下文中我們?cè)僭敿?xì)討論。
我們不妨嘗試在不改變指令塊大小和內(nèi)容的前提下,改變內(nèi)部指令的排布,使得store與load指令不再分布于指令塊的兩端:
str x11, [x10,#0]nopnopnopnopnopldr x11, [x10,#0]nop

有明顯的棧操作消除,現(xiàn)象和之前相比似乎并沒有什么變化。我們更進(jìn)一步,再將load指令向上移動(dòng)一個(gè)槽位:
str x11, [x10,#0]nopnopnopnopldr x11, [x10,#0]nopnop

異常出現(xiàn)了,棧操作消除的效果仍然存在,因?yàn)槠鋱?zhí)行時(shí)長(zhǎng)小于load-to-use延遲,但是又明顯高于之前的延遲,這說明棧操作消除并非百分之百成功。不是每一對(duì)store-load指令都被消除,而是呈一定概率被消除,進(jìn)而將load-to-use延遲與棧操作消除的延遲按一定比例混合了;而在這種情況下,棧操作消除的成功率下降了。我們繼續(xù)將load指令向上移動(dòng),直至與store指令緊鄰:
str x11, [x10,#0]nopnopnopldr x11, [x10,#0]nopnopnop

str x11, [x10,#0]nopnopldr x11, [x10,#0]nopnopnopnop

str x11, [x10,#0]nopldr x11, [x10,#0]nopnopnopnopnop

str x11, [x10,#0]ldr x11, [x10,#0]nopnopnopnopnopnop

我們可以觀察到幾個(gè)異?,F(xiàn)象:
隨著store與load指令的間距越來越小,棧操作消除的成功率越來越低。
當(dāng)store與load指令緊鄰時(shí),棧操作消除機(jī)制近乎完全失效(不發(fā)生)。
load每上移2次,棧操作消除的成功率才變化一次。
這三個(gè)現(xiàn)象包含了海量的信息。我們可以由此推測(cè),蘋果的棧操作消除是基于靜態(tài)操作數(shù)信息的消除,并未使用歷史信息;而且并未優(yōu)化當(dāng)store與load指令在同一周期被譯碼和重命名的情況。
已知隨著store與load指令間距縮小棧操作消除的成功率變低,說明棧操作消除的判斷機(jī)制與指令的槽位有關(guān)。
由1推演可知,指令的槽位實(shí)際上代表了指令在Cache行內(nèi)的位置,不同的位置會(huì)使得它們進(jìn)入處理器后位于指令包內(nèi)的不同槽位。
由于我們構(gòu)造的指令塊總是由nop指令填充至8條(恰好是M1每周期能夠取指和譯碼的指令數(shù)量),我們不妨將一個(gè)指令塊視作處理器流水線內(nèi)每周期得到的指令包。由于nop指令在譯碼級(jí)才會(huì)被消除,因此每個(gè)指令包在到達(dá)譯碼級(jí)時(shí)仍然為8條指令;經(jīng)過譯碼級(jí)后只剩下store與load指令需要重命名。
由3推演可知,在流水線未被塞滿時(shí),每周期我們能夠向處理器的后端填入1條store指令和1條load指令(nop指令在譯碼級(jí)被消除,無需被真正執(zhí)行,直接標(biāo)記等待提交即可);而即便在理想情況下(1.6 cycles per block)棧操作消除也無法使我們每周期執(zhí)行一個(gè)指令塊(1 store + 1 load),因此處理器后端在經(jīng)過一定時(shí)間后會(huì)由于訪存隊(duì)列滿而產(chǎn)生反壓;反壓進(jìn)而導(dǎo)致發(fā)射隊(duì)列和分派隊(duì)列也被填滿產(chǎn)生反壓;最終導(dǎo)致流水線停頓(重命名級(jí)和更早的流水級(jí)阻塞),性能計(jì)數(shù)器的數(shù)值印證了這一點(diǎn):重命名級(jí)經(jīng)歷了相當(dāng)數(shù)量的停頓,約1/3的總cycle數(shù);且基本100%來自分派隊(duì)列的反壓。
5. 由4推演可知,一旦處理器進(jìn)入阻塞狀態(tài),由于每周期只能執(zhí)行并提交平均1.6條指令(不妨以理想狀態(tài)推演)即1-2條指令,可以預(yù)見重命名級(jí)每周期也只能向分派隊(duì)列發(fā)送1-2條指令。同時(shí)由3已知重命名級(jí)最多只會(huì)同時(shí)存在2條指令,因?yàn)閚op指令在進(jìn)入重命名級(jí)前被消除了。
6.已知重命名級(jí)在被清空前無法接受下一個(gè)指令包,否則維護(hù)指令序關(guān)系異常困難。
7. 由1、2、5、6推演可知,M1的譯碼級(jí)支持每周期向重命名級(jí)送入少于一個(gè)指令包的指令。否則,無論指令位于指令包的哪個(gè)槽位,對(duì)于譯碼和重命名級(jí)都是等價(jià)的,不會(huì)造成棧操作消除效果的波動(dòng)。
8. 已知load每上移2次,棧操作消除的成功率才變化一次。
9. 由7和8推演可知,M1的譯碼級(jí)最小支持以2條指令為粒度向重命名級(jí)發(fā)送指令,槽位0-1、2-3、4-5、6-7可以分別進(jìn)入后續(xù)流水級(jí)直至所有指令清空,隨后譯碼級(jí)接受下一個(gè)指令包。
綜上可知,store與load指令的間隔大小與它們同時(shí)進(jìn)入重命名級(jí)的概率密切相關(guān),間隔越小同時(shí)進(jìn)入重命名級(jí)的概率越高,而同時(shí)進(jìn)入重命名級(jí)意味著棧操作消除失敗。由于譯碼級(jí)只支持槽位0-1、2-3、4-5、6-7分別進(jìn)入后續(xù)流水級(jí),因此在槽位移動(dòng)小于2時(shí)對(duì)處理器而言是等價(jià)的,我們不會(huì)觀察到棧操作消除成功率的變化;同時(shí)也意味著一旦store與load指令緊鄰,它們近乎一定會(huì)同時(shí)進(jìn)入重命名級(jí),即一定會(huì)使棧操作消除失效。
至此,我們發(fā)現(xiàn)了蘋果M1棧操作消除存在的證據(jù);初步推演了M1棧操作消除機(jī)制失效的原因。那么為何store與load指令同時(shí)進(jìn)入重命名級(jí)會(huì)使棧操作消除機(jī)制失效呢?我們下篇再見,屆時(shí)將還原M1棧操作消除的實(shí)現(xiàn)細(xì)節(jié),并揭示M1 ICache和流水線內(nèi)設(shè)計(jì)的冰山一角。

(太長(zhǎng)了,字?jǐn)?shù)太多,只好忍痛分成兩篇水了)下篇在此。
JamesAslan:蘋果的黑魔法?(下)Apple M1的棧操作消除98 贊同 · 8 評(píng)論文章
編輯于 2023-01-23 20:23?
首發(fā)于
CPU設(shè)計(jì)逆向——探尋隱秘的故事
蘋果的黑魔法?(下)Apple M1的棧操作消除
JamesAslan
喜歡畫畫和攝影的硅工碼農(nóng)(滑稽)
目錄
收起
回顧
正文
方案對(duì)比
同時(shí)進(jìn)入流水線的影響幾何?
基于歷史or基于靜態(tài)信息?
物理寄存器號(hào)or邏輯寄存器號(hào)?(值信息)
物理寄存器號(hào)or邏輯寄存器號(hào)?(地址信息)
能否對(duì)地址進(jìn)行簡(jiǎn)單運(yùn)算?
能否識(shí)別虛假的棧操作?
后記
回顧
在上篇中我們發(fā)現(xiàn)了M1有著激進(jìn)的棧操作消除機(jī)制,但是其并不完美在部分情形下不能生效;我們由此分析和推演了其失效的原因,并猜想了部分實(shí)現(xiàn)細(xì)節(jié)。本篇我們將還原M1棧操作消除的實(shí)現(xiàn)細(xì)節(jié),并揭示M1 ICache和流水線內(nèi)設(shè)計(jì)的冰山一角。
蘋果的黑魔法?Apple M1的棧操作消除(上)366 贊同 · 21 評(píng)論文章
正文
為何store與load指令同時(shí)進(jìn)入重命名級(jí)會(huì)使棧操作消除失效呢?這與棧操作消除的實(shí)現(xiàn)機(jī)制密切相關(guān)。
棧操作消除可以基于兩種較為簡(jiǎn)單的思路: 1. 基于靜態(tài)信息,例如操作數(shù)的源寄存器號(hào)。倘若一對(duì)store與load指令用于計(jì)算地址的源寄存器和偏移都相同,那我們顯然可以推測(cè)store與load訪問了同一地址,并對(duì)它們進(jìn)行消除。消除的方式多種多樣,較為簡(jiǎn)單的即直接通過重命名機(jī)制,將load指令的目的寄存器重命名為store指令的值的源寄存器;但是為了確保這樣的消除是正確的,load指令仍然需要被執(zhí)行以驗(yàn)證地址的合法性以及它們真的相等,因此只是消除了執(zhí)行延遲而并非整個(gè)指令的執(zhí)行。 2. 基于歷史信息,例如指令PC。倘若一對(duì)store與load指令曾經(jīng)訪問過相同的物理地址,那我們將它們標(biāo)記為可能再次訪問同一地址,并對(duì)它們進(jìn)行消除。消除的方式可以與上一點(diǎn)近似,同樣其也需要被執(zhí)行來驗(yàn)證正確性。
無論使用了哪種思路,棧操作消除機(jī)制都由兩個(gè)基本部分組成:消除識(shí)別器(以下簡(jiǎn)稱識(shí)別器)和信息暫存器(以下簡(jiǎn)稱暫存器)。上述兩種思路的區(qū)別主要體現(xiàn)在識(shí)別器上,基于靜態(tài)信息的識(shí)別器與暫存器高度耦合,其能使用的信息全部來自于暫存器內(nèi)存儲(chǔ)的內(nèi)容;而基于歷史信息的識(shí)別器則可以利用其本身存儲(chǔ)的執(zhí)行歷史信息,這些執(zhí)行歷史信息可以以PC作為索引,記錄曾經(jīng)訪問過相同物理地址的store與load指令,能夠提供比靜態(tài)信息更多的消除機(jī)會(huì)。
方案對(duì)比
對(duì)于基于靜態(tài)信息的棧操作消除機(jī)制,同時(shí)進(jìn)入重命名級(jí)的store與load指令對(duì)會(huì)極大考驗(yàn)其設(shè)計(jì)。因?yàn)橹挥挟?dāng)store指令的信息被存儲(chǔ)進(jìn)入暫存器后,識(shí)別器才能嘗試將后續(xù)load與先前的store信息進(jìn)行匹配,而存儲(chǔ)這個(gè)操作是需要一整個(gè)時(shí)鐘周期去完成的。倘若需要消除同一個(gè)指令包內(nèi)的store與load指令對(duì),就要在譯碼或重命名級(jí)進(jìn)行指令間的兩兩復(fù)雜檢查,這種檢查的復(fù)雜度會(huì)隨著處理器寬度的增長(zhǎng)指數(shù)增長(zhǎng)。蘋果在近些年的確有類似的專利,但是從實(shí)測(cè)結(jié)果看(見上篇的測(cè)試)M1并沒有實(shí)裝這些專利。
對(duì)于基于歷史信息的棧操作消除機(jī)制,同時(shí)進(jìn)入重命名級(jí)的store與load指令相對(duì)容易處理。因?yàn)樽R(shí)別器可以根據(jù)存儲(chǔ)的PC提前識(shí)別可能的store與load指令,不需要指令包內(nèi)指令間的兩兩檢查而是一對(duì)一的PC比較。但是從上篇的實(shí)測(cè)結(jié)果來看,M1對(duì)于同時(shí)進(jìn)入重命名級(jí)的store與load指令近乎毫無招架之力,不像是使用了這一方案,后文我們使用測(cè)試來進(jìn)行排除。
假設(shè)M1使用了基于靜態(tài)信息的棧操作消除機(jī)制,暫存器內(nèi)會(huì)存儲(chǔ)什么信息呢?我們需要兩大類信息:
地址信息,即store指令訪問了什么地址。由于不參考?xì)v史信息,我們無從得知物理地址信息僅有虛地址信息;而虛地址由base+offset生成,base與offset分別來自寄存器號(hào)與立即數(shù)。那么結(jié)果似乎顯而易見了,我們需要存儲(chǔ)用于生成store指令訪問地址的寄存器號(hào)和立即數(shù);但是寄存器號(hào)又有物理寄存器號(hào)(經(jīng)過重命名后的硬件內(nèi)部使用的寄存器)和邏輯寄存器號(hào)(代碼中可見的ISA規(guī)定的寄存器)之分,M1到底使用了什么下文我們測(cè)試后才能確定。
值信息,即store指令存儲(chǔ)了什么內(nèi)容。為了在重命名時(shí)消除load指令,我們必須將store指令的值來源物理寄存器直接傳遞給load指令,并將load指令的結(jié)果寄存器重命名為該物理寄存器。但是暫存器未必直接存儲(chǔ)了物理寄存器號(hào),其仍然可以只存儲(chǔ)邏輯寄存器號(hào),并在load指令進(jìn)入重命名級(jí)后讓其讀取暫存的邏輯寄存器號(hào)對(duì)應(yīng)的物理寄存器號(hào);代價(jià)就是,一旦store與load指令間有任何其他指令修改過被暫存的邏輯寄存器,那么重命名表的內(nèi)容就會(huì)被更新,load指令就不能通過前述方式獲得正確的物理寄存器號(hào),進(jìn)而導(dǎo)致棧操作消除失敗。暫存器存儲(chǔ)物理寄存器號(hào)需要讀取重命名表,因此設(shè)計(jì)會(huì)更加復(fù)雜;相應(yīng)的,能夠覆蓋的棧操作消除情況也會(huì)變多。M1到底使用了什么下文我們測(cè)試后才能確定。
有了這么多的猜想和疑惑,我們現(xiàn)在開始一一測(cè)試。
同時(shí)進(jìn)入流水線的影響幾何?
我們不斷提及,同時(shí)進(jìn)入重命名級(jí)的store與load指令會(huì)導(dǎo)致棧操作消除失??;但是即便是此前測(cè)試過的在同一指令包中store與load相距最遠(yuǎn)的情況(如下),也有相當(dāng)概率的消除失?。ㄒ?yàn)閏ycles per block為1.6,非整數(shù))。
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
ldr x11, [x10,#0]
我們必須嘗試構(gòu)建一種極端情況:在指令塊指令數(shù)量不變(8條)的情況下,使得同一指令塊內(nèi)的store指令和load指令不在同一周期進(jìn)入流水線,即這兩條指令不位于流水線內(nèi)的同一指令包內(nèi)。我們觀察此時(shí)的反匯編:
100004500: 0b 01 00 f9 str x11, [x8] //內(nèi)循環(huán)開始
100004504: 1f 20 03 d5 nop
100004508: 1f 20 03 d5 nop
10000450c: 1f 20 03 d5 nop
100004510: 1f 20 03 d5 nop
100004514: 1f 20 03 d5 nop
100004518: 1f 20 03 d5 nop
10000451c: 0b 01 40 f9 ldr x11, [x8]
每一個(gè)指令塊被放置于32Byte對(duì)齊的位置,即每一個(gè)指令塊的頭指令恰好位于cache行的開頭或一半;這是十分有利于取指的排布。但是有利于取指也就意味著它們總是作為一個(gè)指令包被取入流水線內(nèi),這恰恰是我們不希望見到的;我們不妨讓其排布不再如此對(duì)齊,進(jìn)而恰好使一個(gè)指令塊的8條指令無法被同時(shí)取入流水線。我們?cè)诒粶y(cè)指令前加入一條無用的占位指令,破壞cache行的排布:
100004500: 8a 01 00 91 add x10, x12, #0 //占位指令
100004504: 0b 01 00 f9 str x11, [x8] ? ?//內(nèi)循環(huán)開始
100004508: 1f 20 03 d5 nop
10000450c: 1f 20 03 d5 nop
100004510: 1f 20 03 d5 nop
100004514: 1f 20 03 d5 nop
100004518: 1f 20 03 d5 nop
10000451c: 1f 20 03 d5 nop
100004520: 0b 01 40 f9 ldr x11, [x8]
性能大幅提升!可見在這種狀態(tài)下棧操作消除的成功率近乎完美。這說明了兩個(gè)問題:
同時(shí)進(jìn)入重命名級(jí)的store與load指令確實(shí)會(huì)導(dǎo)致棧操作消除失敗。
M1的取指并不能從ICache行內(nèi)的任意位置開始。
我們著重說明第二點(diǎn)。倘若M1的取指能夠從ICache行內(nèi)的任意位置開始,那么在外循環(huán)第一次回跳到內(nèi)循環(huán)開始處時(shí),無論指令塊是否按照Cache行對(duì)齊,對(duì)于處理器而言都是等價(jià)的;因?yàn)樘幚砥骺梢垣@得在第n行的前7條指令和第n+1行的第8條指令(行內(nèi)一半位置時(shí)同理)。但是測(cè)試結(jié)果明顯體現(xiàn)了不等價(jià)性,這里的不等價(jià)性也意味著M1的ICache分bank粒度并未到單個(gè)4 Byte指令的粒度。那么M1的ICache bank粒度到底是多大呢?我們不妨再插入一條占位指令:
100004500: 8a 01 00 91 add x10, x12, #0 //占位指令
100004504: 8a 01 00 91 add x10, x12, #0 //占位指令
100004508: 0b 01 00 f9 str x11, [x8] ? ?//內(nèi)循環(huán)開始
10000450c: 1f 20 03 d5 nop
100004510: 1f 20 03 d5 nop
100004514: 1f 20 03 d5 nop
100004518: 1f 20 03 d5 nop
10000451c: 1f 20 03 d5 nop
100004520: 1f 20 03 d5 nop
100004524: 0b 01 40 f9 ldr x11, [x8]
性能又衰退回到了最初沒有插入占位指令的水平。這意味著這種指令排布對(duì)于處理器而言與未插入占位指令時(shí)是等價(jià)的;即處理器在跨行時(shí)也可以取到同一指令塊的8條指令(在第n行的末尾獲得前6條指令,在第n+1行的開頭獲得第7、8條指令);所以,M1的ICache bank粒度為2條指令即8 Byte。這一粒度對(duì)于ICache而言已經(jīng)十分細(xì)膩了,作為參考AMD Zen 4的取指粒度為16 Byte。
基于歷史or基于靜態(tài)信息?
從前文的實(shí)測(cè)結(jié)果來看,M1對(duì)于同時(shí)進(jìn)入重命名級(jí)的store與load指令近乎毫無招架之力,不具有使用了歷史信息來幫助棧操作消除的特征,我們還可以嘗試從另一個(gè)角度驗(yàn)證這一點(diǎn)。任何記錄歷史信息的硬件表項(xiàng)都必然是容量有限的;在使用PC進(jìn)行識(shí)別時(shí),如果需要消除的PC不同的store與load指令對(duì)過多,棧操作消除就會(huì)失效(表項(xiàng)被占滿后相互擠兌,導(dǎo)致所有PC都不能被正常識(shí)別,因?yàn)樗鼈冊(cè)谠俅伪粓?zhí)行前就會(huì)被其他PC替換出表)。在上篇測(cè)試構(gòu)建部分我們提及測(cè)試代碼分為內(nèi)循環(huán)與外循環(huán),只要我們?cè)黾觾?nèi)循環(huán)N的數(shù)量,即相當(dāng)于增加了不同PC的store與load指令對(duì)。
//測(cè)試程序示意,實(shí)際程序?yàn)榍度雲(yún)R編:
for(int i=0;i<M;i++){
? ?//共N組store和load
? ?指令塊0
? ?指令塊1
? ?...........
? ?指令塊N-2
? ?指令塊N-1
}
指令塊內(nèi)容方面,我們繼續(xù)使用M1能夠較好消除的間隔最遠(yuǎn)的模式,并插入一個(gè)占位指令(見上一小節(jié))破壞Cache行排布:
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
ldr x11, [x10,#0]
不斷增加內(nèi)循環(huán)數(shù)量(即使用的store與load指令對(duì)的數(shù)量):
//20對(duì)
cycles per block: 1.042
//50對(duì)
cycles per block: 1.032
//100對(duì)
cycles per block: 1.015
//500對(duì)
cycles per block: 1.025
//1000對(duì)
cycles per block: 1.002
//5000對(duì)
cycles per block: 1.003
可見棧操作消除機(jī)制完美生效,因此海量的不同PC并沒有導(dǎo)致某種硬件表結(jié)構(gòu)溢出,我們可以基本確定M1僅使用了靜態(tài)信息來進(jìn)行棧操作消除。
實(shí)際上,M1的store order violation predictor也僅有70項(xiàng)左右的容量,存在超過5000項(xiàng)的棧操作消除歷史表是與前者矛盾的。(store order violation predictor 并非本篇內(nèi)容因此這里不做解釋)
物理寄存器號(hào)or邏輯寄存器號(hào)?(值信息)
值信息,即store指令存儲(chǔ)了什么內(nèi)容。為了在重命名時(shí)消除load指令,我們必須將store指令的值來源物理寄存器傳遞給load指令,并將load指令的結(jié)果寄存器重命名為該物理寄存器。但是暫存器未必直接存儲(chǔ)store的物理寄存器號(hào),其仍然可以只存儲(chǔ)邏輯寄存器號(hào),并在load指令進(jìn)入重命名級(jí)后讓其自行查找與暫存的邏輯寄存器號(hào)對(duì)應(yīng)的物理寄存器號(hào);代價(jià)就是一旦store與load指令間有任何其他指令修改過暫存邏輯寄存器,重命名表的內(nèi)容就會(huì)被更新,load指令就不能通過前述方式獲得正確的物理寄存器號(hào),進(jìn)而導(dǎo)致棧操作消除失敗。那么我們可以在store與load指令間插入一條修改store指令值來源寄存器的指令(不妨記為指令A(yù)),來讓棧操作消除失敗。為了確保指令A(yù)在load指令進(jìn)入重命名級(jí)前就修改完成重命名表,需要讓指令A(yù)提前于load進(jìn)入流水線。我們將指令塊擴(kuò)展至16條指令,理論上它需要兩個(gè)周期才能被取指:
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
nop
// 以上為第一個(gè)周期的取指內(nèi)容;以下為第二個(gè)周期的取指內(nèi)容
nop
nop
nop
nop
nop
nop
nop
ldr x11, [x10,#0]
棧操作消除完美工作,2周期的執(zhí)行時(shí)長(zhǎng)已經(jīng)等于理論最短執(zhí)行延遲,load-to-use的延遲完全不可見。此時(shí)加入指令A(yù),并使其先于load一個(gè)周期進(jìn)入流水線:
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
add x11, x10, #0
// 以上為第一個(gè)周期的取指內(nèi)容;以下為第二個(gè)周期的取指內(nèi)容
nop
nop
nop
nop
nop
nop
nop
ldr x11, [x10,#0]

棧操作消除完美工作,因此M1在值信息方面應(yīng)該使用了物理寄存器號(hào),暫存器內(nèi)很可能存儲(chǔ)了store指令值信息的物理寄存器號(hào);這樣的設(shè)計(jì)十分合理,畢竟棧操作之間程序很可能進(jìn)行了復(fù)雜的行為,需要保證值信息寄存器不被修改十分不合理。這樣的設(shè)計(jì)下暫存器本身很可能被放置在了重命名級(jí),以便利用現(xiàn)有的重命名表讀端口。
物理寄存器號(hào)or邏輯寄存器號(hào)?(地址信息)
地址信息,即store指令訪問了什么地址。由于不參考?xì)v史信息,我們無從得知物理地址信息,僅有虛地址信息;而虛地址由base+offset生成,base與offset分別來自寄存器號(hào)與立即數(shù)。暫存器內(nèi)存儲(chǔ)的是物理寄存器號(hào)還是邏輯寄存器號(hào)呢?我們不妨將store與load指令的base寄存器更換為不同的邏輯寄存器號(hào),但是它們的內(nèi)容相同,并且在初始化時(shí)由同一邏輯寄存器move而來;這樣雖然store與load的base邏輯寄存器號(hào)不同,但是經(jīng)過重命名后它們的物理寄存器號(hào)是一樣的,倘若此時(shí)棧操作消除還能正常工作,則M1是通過物理寄存器號(hào)識(shí)別可以消除的棧操作。首先我們不加入mov指令:
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
ldr x11, [x8,#0] //注意:x8的值與x10相同,即store與load仍然訪問了同一地址

棧操作消除機(jī)制不在工作,我們加入mov指令:
mov x8,x10 ? ? ?//注意:x10對(duì)應(yīng)的物理寄存器被同時(shí)復(fù)制給了x8
str x11, [x10,#0]
nop
nop
nop
nop
nop
nop
ldr x11, [x8,#0] //注意:x8的值與x10相同,即store與load仍然訪問了同一地址

棧操作消除機(jī)制仍然不在工作,因此M1在地址信息方面使用的是邏輯寄存器號(hào);這一選擇也十分合理,使用物理寄存器號(hào)進(jìn)行識(shí)別匹配意味著需要在重命名級(jí)之后才能進(jìn)行,M1較短的流水線不便使用類似設(shè)計(jì),否則會(huì)帶來巨大的時(shí)序壓力(處理器頻率無法提高)。
能否對(duì)地址進(jìn)行簡(jiǎn)單運(yùn)算?
我們?cè)趕tore與load指令間假意修改用于生成訪存地址的base寄存器:
str x11, [x10,#0]
nop
nop
nop
mov x10, x10 // 實(shí)際上并沒有改變x10的值
nop
nop
ldr x11, [x10,#0]

棧操作消除立即失效,因此M1的棧操作消除機(jī)制不能容忍棧操作之間對(duì)地址的任何操作。
能否識(shí)別虛假的棧操作?
我們?cè)跅2僮髦噶睿ㄔL問地址A,則其修改的地址空間為A:A+8字節(jié))之間,加入一條store指令(不妨記為指令B)且訪問地址A+4(則其修改的地址空間為A+4:A+12字節(jié));這樣load指令實(shí)際上會(huì)受到指令B的影響,其需要從地址A:A+4取回第一條store存入的內(nèi)容,從地址A+4:A+8取回指令B存入的內(nèi)容;倘若使用棧操作消除機(jī)制直接將第一條store的值傳遞給load,就會(huì)導(dǎo)致錯(cuò)誤:
str x11, [x10,#0]
str x12, [x10,#4] // 地址偏移了4字節(jié),與棧操作的地址部分重疊卻不相等
nop
nop
nop
nop
nop
ldr x11, [x10,#0]


可以看到不但棧操作消除不在生效,而且出現(xiàn)了大量的流水線刷新,該性能計(jì)數(shù)器事件排除了store order violation和分支誤預(yù)測(cè),那么顯然就是棧操作消除誤判產(chǎn)生的回滾。因此M1不能識(shí)別部分不該進(jìn)行棧操作消除的場(chǎng)景,處理器在驗(yàn)證后會(huì)因此刷新流水線。(其進(jìn)行了棧操作消除,但是在后續(xù)驗(yàn)證過程中發(fā)現(xiàn)了錯(cuò)誤,因此刷新流水線回滾并重新執(zhí)行l(wèi)oad)
后記
M1的棧操作消除機(jī)制讓人大開眼界,但最新的x86陣營(yíng)處理器在這一方面遠(yuǎn)比蘋果激進(jìn)(它們的故事我們以后再講);不過蘋果的實(shí)現(xiàn)方式與它們并不相同,這有可能是ARM ISA本身的特性或是實(shí)際需求使然。從測(cè)試中看M1的相關(guān)機(jī)制還遠(yuǎn)算不上完善,仍然有較多的corner case亟待優(yōu)化。希望蘋果早日結(jié)束擠牙膏操作,重回業(yè)界攪局者的角色,Ultra已來Extreme何在?

編輯于 2023-02-04 11:26?IP 屬地江蘇
蘋果公司 (Apple Inc.)
蘋果產(chǎn)品
中央處理器 (CPU)