[英]Are there any modern CPUs where a cached byte store is actually slower than a word store?
一個普遍的說法是,緩存中的字節存儲可能導致內部讀 - 修改 - 寫周期,或者與存儲完整寄存器相比會損害吞吐量或延遲。
但我從未見過任何例子。 沒有x86 CPU是這樣的,我認為所有高性能CPU也可以直接修改緩存行中的任何字節。 一些微控制器或低端CPU是否有不同之處,如果它們有緩存的話?
( 我不計算字可尋址的機器 ,或Alpha,它是字節可尋址但缺少字節加載/存儲指令。我在談論ISA本身支持的最窄的存儲指令。)
在我的研究中回答現代x86硬件可以不將單個字節存儲到內存中嗎? ,我發現Alpha AXP省略字節存儲的原因假設它們被實現為真正的字節存儲到緩存中,而不是包含字的RMW更新。 (因此,它會使L1d緩存的ECC保護更加昂貴,因為它需要字節粒度而不是32位)。
我假設在提交到L1d緩存期間,word-RMW不被視為實現字節存儲的其他更新的ISA的實現選項。
所有現代架構(早期Alpha除外)都可以對不可緩存的MMIO區域(而不是RMW周期)執行真正的字節加載/存儲,這對於為具有相鄰字節I / O寄存器的設備編寫設備驅動程序是必需的。 (例如,使用外部啟用/禁用信號來指定更寬總線的哪些部分保存實際數據,例如此ColdFire CPU /微控制器上的2位TSIZ(傳輸大小),或者像PCI / PCIe單字節傳輸,或者像DDR一樣SDRAM控制信號掩蓋選定的字節。)
對於微控制器設計,可能需要在緩存中為字節存儲執行RMW循環,即使它不是針對像Alpha這樣的SMP服務器/工作站的高端超標量流水線設計?
我認為這種說法可能來自可以用字尋址的機器。 或者來自未對齊的32位存儲,需要在許多CPU上進行多次訪問,並且人們錯誤地將其從一般存儲到字節存儲。
為了清楚起見,我希望到同一地址的字節存儲循環將在每次迭代中以與字存儲循環相同的周期運行。 因此,對於填充陣列,32位存儲可以比8位存儲快4倍。 (也許如果少了32位門店飽和的內存帶寬,但8位店家沒有。)但是,除非字節存儲有一個額外的懲罰,你不會得到超過4倍速度差多 。 (或者無論寬度是多少)。
而我在談論asm。 一個好的編譯器將自動向量化C中的字節或int存儲循環,並使用更寬的存儲或目標ISA上的最佳存儲,如果它們是連續的。
(並且在存儲緩沖區中存儲合並也可能導致對連續字節存儲指令的L1d高速緩存的更寬提交,因此在微基准測試時需要注意另一件事)
; x86-64 NASM syntax
mov rdi, rsp
; RDI holds at a 32-bit aligned address
mov ecx, 1000000000
.loop: ; do {
mov byte [rdi], al
mov byte [rdi+2], dl ; store two bytes in the same dword
; no pointer increment, this is the same 32-bit dword every time
dec ecx
jnz .loop ; }while(--ecx != 0}
mov eax,60
xor edi,edi
syscall ; x86-64 Linux sys_exit(0)
或者像這樣循環一個8kiB數組,每8個字節存儲1個字節或1個字(對於一個C實現,sizeof(unsigned int)= 4而CHAR_BIT = 8用於8kiB,但是應該編譯為任何類似的函數C實現,如果sizeof(unsigned int)
不是2的冪,則只有很小的偏差。 在Godbolt上針對幾種不同的ISA進行 ASM,無論是否展開,或兩種版本的展開量相同。
// volatile defeats auto-vectorization
void byte_stores(volatile unsigned char *arr) {
for (int outer=0 ; outer<1000 ; outer++)
for (int i=0 ; i< 1024 ; i++) // loop over 4k * 2*sizeof(int) chars
arr[i*2*sizeof(unsigned) + 1] = 123; // touch one byte of every 2 words
}
// volatile to defeat auto-vectorization: x86 could use AVX2 vpmaskmovd
void word_stores(volatile unsigned int *arr) {
for (int outer=0 ; outer<1000 ; outer++)
for (int i=0 ; i<(1024 / sizeof(unsigned)) ; i++) // same number of chars
arr[i*2 + 0] = 123; // touch every other int
}
根據需要調整大小,如果有人能指向word_store()
比byte_store()
更快的系統,我真的很好奇。 (如果實際是基准測試,請注意動態時鍾速度等熱身效應,以及觸發TLB未命中和緩存未命中的第一次傳遞。)
或者,如果不存在古代平台的實際C編譯器或生成不會對商店吞吐量造成瓶頸的次優代碼,那么任何手工制作的asm都會顯示效果。
任何其他證明字節存儲速度減慢的方法都很好,我不堅持在數組上進行跨步循環或在一個單詞中發送垃圾郵件。
關於CPU內部的詳細文檔或不同指令的CPU周期時序數, 我也可以 。 不過,我對未經測試的可能基於此聲明的優化建議或指南持懷疑態度。
例如,這是ARM Cortex-A的情況? 還是Cortex-M? 任何舊的ARM微體系結構? 任何MIPS微控制器或早期的MIPS服務器/工作站CPU? 任何其他隨機RISC如PA-RISC,或像VAX或486這樣的CISC? (CDC6600可以進行單詞尋址。)
或者構建一個涉及負載和存儲的測試用例,例如顯示來自字節存儲的word-RMW與負載吞吐量競爭。
(我對顯示從字節存儲到字加載的存儲轉發比字 - >字更慢感興趣,因為正常情況下SF只有當負載完全包含在最近的存儲中才能觸及任何相關的字節。但是顯示字節 - >字節轉發效率低於字 - >字SF的東西會很有趣,可能是字節不是從字邊界開始的。)
( 我沒有提到字節加載,因為這通常很簡單 :從緩存或RAM中訪問一個完整的字然后提取你想要的字節。除了MMIO之外,這個實現細節是無法區分的,其中CPU肯定不會讀取包含的字。 )
在像MIPS這樣的加載/存儲架構上,使用字節數據只是意味着你使用lb
或lbu
加載並對其進行歸零或符號擴展,然后用sb
存儲它。 (如果你需要在寄存器中的步驟之間截斷8位,那么你可能需要一個額外的指令,因此本地變量通常應該是寄存器大小。除非你希望編譯器使用8位元素的SIMD自動向量化,然后經常uint8_t本地人很好......)但無論如何,如果你做得對,你的編譯器是好的,它不應該花費任何額外的指令來擁有字節數組。
我注意到gcc在ARM,AArch64,x86和MIPS上的sizeof(uint_fast8_t) == 1
。 但IDK我們可以投入多少庫存。 x86-64 System V ABI在x86-64 uint_fast32_t
定義為64位類型。 如果他們要這樣做(而不是32位,這是x86-64的默認操作數大小), uint_fast8_t
也應該是64位類型。 當用作數組索引時,可能避免零擴展? 如果它作為函數arg在寄存器中傳遞,因為如果你不得不從內存中加載它,它可以免費零擴展。
我猜是錯的。 現代x86微體系結構在某種程度上與一些(大多數?)其他ISA非常不同。
即使在高性能的非x86 CPU上,緩存的窄存儲也會受到懲罰。 但是,緩存占用空間的減少仍然可以使int8_t
數組值得使用。 (對於像MIPS這樣的一些ISA,不需要為尋址模式擴展索引有幫助)。
在字節之間將存儲緩沖區中的合並/合並在實際提交到L1d之前將指令存儲到相同的字也可以減少或消除懲罰。 (x86有時不能做到這一點,因為它強大的內存模型要求所有商店按程序順序提交。)
ARM的Cortex-A15 MPCore文檔 (自2012年起)表示它在L1d中使用32位ECC粒度,並且實際上為窄存儲執行word-RMW更新數據。
L1數據高速緩存支持標簽和數據陣列中的可選單位校正和雙位檢測糾錯邏輯。 標簽陣列的ECC粒度是單個高速緩存行的標記,數據陣列的ECC粒度是32位字。
由於數據陣列中的ECC粒度,對數組的寫入不能更新4字節對齊的存儲器位置的一部分,因為沒有足夠的信息來計算新的ECC值。 對於沒有寫入一個或多個對齊的4字節存儲區域的任何存儲指令就是這種情況。 在這種情況下,L1數據存儲器系統讀取高速緩存中的現有數據,合並修改的字節,並根據合並的值計算ECC。 L1存儲器系統嘗試將多個存儲器合並在一起以滿足對齊的4字節ECC粒度並避免讀取 - 修改 - 寫入要求。
(當他們說“L1內存系統”時,我認為它們意味着存儲緩沖區,如果你有連續的字節存儲尚未提交給L1d。)
請注意,RMW是原子的,只涉及被修改的獨占高速緩存行。 這是一個不影響內存模型的實現細節。 那么我對Can現代x86硬件的結論是不是將單個字節存儲到內存中? 仍然(可能)正確x86可以,所以每個其他ISA提供字節存儲指令。
Cortex-A15 MPCore是一個3路無序執行CPU,因此它不是最小的功耗/簡單的ARM設計,但他們選擇在OoO exec上花費晶體管而不是高效的字節存儲。
假設不需要支持高效的未對齊存儲(x86軟件更可能采用/利用),具有較慢的字節存儲被認為是值得的,因為L1d的ECC具有更高的可靠性而沒有過多的開銷。
Cortex-A15可能不是以這種方式工作的唯一且不是最新的ARM內核。
其他例子(由@HadiBrais在評論中找到):
Alpha 21264 (參見本文檔第8章表8-1)的L1d緩存具有8字節ECC粒度。 較窄的存儲(包括32位)在它們提交到L1d時會產生RMW,如果它們沒有首先在存儲緩沖區中合並。 該文檔解釋了每個時鍾L1d可以做什么的完整細節。 特別是商店緩沖區合並商店的文檔。
PowerPC RS64-II和RS64-III (參見本文檔中的錯誤部分)。 根據該摘要 ,RS / 6000處理器的L1對於每32位數據具有7位ECC。
Alpha從頭開始是積極的64位,因此8字節粒度有一定意義,特別是如果RMW成本大部分可以被存儲緩沖區隱藏/吸收。 (例如,對於該CPU上的大多數代碼而言,正常的瓶頸可能在其他地方;其多端口緩存通常可以在每個時鍾處理2個操作。)
POWER / PowerPC64源於32位PowerPC,可能關心運行32位代碼和32位整數和指針。 (因此更有可能對無法合並的數據結構執行非連續的32位存儲。)因此,32位ECC粒度在那里很有意義。
cortex-m7 trm,手冊的緩存ram部分。
在無差錯系統中,主要的性能影響是數據端非完整存儲的讀 - 修改 - 寫方案的成本。 如果存儲緩沖器槽不包含至少一個完整的32位字,則它必須讀取該字以便能夠計算校驗位。 這可能是因為軟件僅使用字節或半字存儲指令寫入存儲器區域。 然后可以將數據寫入RAM。 此附加讀取可能會對性能產生負面影響,因為它會阻止插槽用於另一次寫入。
。
存儲器系統的緩沖和突出功能掩蓋了附加讀取的一部分,對於大多數代碼來說它可以忽略不計。 但是,ARM建議您盡可能使用可緩存的STRB和STRH指令來降低性能影響。
我有皮質-m7s,但到目前為止還沒有進行過測試來證明這一點。
“讀取單詞”是什么意思,它是SRAM中一個存儲位置的讀取,它是數據高速緩存的一部分。 它不是一個高級系統內存的東西。
緩存的內部是圍繞SRAM塊構建的,SRAM塊是快速SRAM,它使緩存成為現實,比系統內存更快,快速將答案返回給處理器等。這種讀取 - 修改 - 寫入(RMW)不是高級寫政策的事情。 他們所說的是如果有命中並且寫策略說要將寫保存在高速緩存中,則需要將字節或半字寫入這些SRAM中的一個。 如本文所示,具有ECC的數據高速緩存數據SRAM的寬度為32 + 7位寬。 32位數據7位ECC校驗位。 您必須將所有39位保持在一起才能使ECC工作。 根據定義,您不能僅修改某些位,因為這會導致ECC錯誤。
每當存儲在數據高速緩存數據SRAM,8,16或32位中的32位字需要改變任何數量的位時,必須重新計算7個校驗位並且一次寫入所有39位。 對於8位或16位,STRB或STRH寫入,32位數據需要讀取8或16位,修改后該字中的其余數據位不變,計算7個ECC校驗位,並將39位寫入sram 。
檢查位的計算理想地/可能在設置寫入的相同時鍾周期內,但讀取和寫入不在同一時鍾周期中,因此至少需要兩個單獨的周期來寫入到達高速緩存的數據在一個時鍾周期。 有些技巧可以延遲寫入,有時也可能會造成傷害,但通常會將其移動到一個未使用過的循環中,如果你願意的話可以使它自由。 但它不會與讀取時鍾周期相同。
他們說如果你抓住你的嘴並設法讓足夠小的商店足夠快地到達緩存,他們將停止處理器,直到它們能夠趕上。
該文檔還描述了無ECC SRAM為32位寬,這意味着在沒有ECC支持的情況下編譯內核時也是如此。 我無法訪問此內存接口的信號或文檔,所以我無法肯定地說,但如果它實現為沒有字節通道控件的32位寬接口,那么你有同樣的問題,它只能寫一個完整的32位項目這個SRAM而不是分數所以要改變8或16位你必須RMW,在緩存的內部。
簡單回答為什么不使用更窄的內存,芯片的大小,ECC的大小加倍,因為即使寬度越來越小,你可以使用多少檢查位的限制(每8位7位是更多比特每32比特保存7比特。 內存越窄,你就會有更多的信號路由,並且無法密集地將內存打包。 一套公寓和一堆獨立的房子可以容納相同數量的人。 前門的道路和人行道而不是走廊。
並且esp使用這樣的單核處理器,除非你有意嘗試(我會),你不可能不小心碰到這個,為什么要把產品的成本提高到:它可能不會發生?
請注意,即使使用多核處理器,您也會看到這樣的內存。
編輯。
好的開始測試了。
0800007c <lwtest>:
800007c: b430 push {r4, r5}
800007e: 6814 ldr r4, [r2, #0]
08000080 <lwloop>:
8000080: 6803 ldr r3, [r0, #0]
8000082: 6803 ldr r3, [r0, #0]
8000084: 6803 ldr r3, [r0, #0]
8000086: 6803 ldr r3, [r0, #0]
8000088: 6803 ldr r3, [r0, #0]
800008a: 6803 ldr r3, [r0, #0]
800008c: 6803 ldr r3, [r0, #0]
800008e: 6803 ldr r3, [r0, #0]
8000090: 6803 ldr r3, [r0, #0]
8000092: 6803 ldr r3, [r0, #0]
8000094: 6803 ldr r3, [r0, #0]
8000096: 6803 ldr r3, [r0, #0]
8000098: 6803 ldr r3, [r0, #0]
800009a: 6803 ldr r3, [r0, #0]
800009c: 6803 ldr r3, [r0, #0]
800009e: 6803 ldr r3, [r0, #0]
80000a0: 3901 subs r1, #1
80000a2: d1ed bne.n 8000080 <lwloop>
80000a4: 6815 ldr r5, [r2, #0]
80000a6: 1b60 subs r0, r4, r5
80000a8: bc30 pop {r4, r5}
80000aa: 4770 bx lr
每個都有一個加載字(ldr),加載字節(ldrb),存儲字(str)和存儲字節(strb)版本,每個都至少在16字節邊界上對齊,直到循環地址的頂部。
啟用icache和dcache
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=lbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=swtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=sbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0001000B
00010007
0001000B
00010007
0001000C
00010007
0002FFFD
0002FFFD
雖然這些商店的堆積如此,但是一個字節寫入比單詞寫入長3倍。
但如果你不打那么難的緩存
0800019c <nbtest>:
800019c: b430 push {r4, r5}
800019e: 6814 ldr r4, [r2, #0]
080001a0 <nbloop>:
80001a0: 7003 strb r3, [r0, #0]
80001a2: 46c0 nop ; (mov r8, r8)
80001a4: 46c0 nop ; (mov r8, r8)
80001a6: 46c0 nop ; (mov r8, r8)
80001a8: 7003 strb r3, [r0, #0]
80001aa: 46c0 nop ; (mov r8, r8)
80001ac: 46c0 nop ; (mov r8, r8)
80001ae: 46c0 nop ; (mov r8, r8)
80001b0: 7003 strb r3, [r0, #0]
80001b2: 46c0 nop ; (mov r8, r8)
80001b4: 46c0 nop ; (mov r8, r8)
80001b6: 46c0 nop ; (mov r8, r8)
80001b8: 7003 strb r3, [r0, #0]
80001ba: 46c0 nop ; (mov r8, r8)
80001bc: 46c0 nop ; (mov r8, r8)
80001be: 46c0 nop ; (mov r8, r8)
80001c0: 3901 subs r1, #1
80001c2: d1ed bne.n 80001a0 <nbloop>
80001c4: 6815 ldr r5, [r2, #0]
80001c6: 1b60 subs r0, r4, r5
80001c8: bc30 pop {r4, r5}
80001ca: 4770 bx lr
然后單詞和字節占用相同的時間
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nwtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
ra=nbtest(0x20002000,0x1000,STK_CVR); hexstring(ra%0x00FFFFFF);
0000C00B
0000C007
0000C00B
0000C007
所有其他因素保持不變,它仍然需要4倍的時間來完成字節,但這就是讓字節占用時間超過4倍的挑戰。
正如我在此問題之前所描述的那樣,您將看到srams是緩存中的最佳寬度以及其他位置,並且字節寫入將遭受讀取 - 修改 - 寫入。 現在,對於其他開銷或優化是否可見,這是另一個故事。 ARM清楚地表明它可能是可見的,我覺得我已經證明了這一點。 這對ARM的設計沒有任何負面影響,事實上相反,RISC一般會在指令/執行方面進行開銷,它需要更多指令來完成相同的任務。 設計的效率允許這樣的事情可見。 有關於如何使你的x86更快,沒有為這個或那個做8位操作,或者其他指令是首選等等的全書,這意味着你應該能夠編寫基准來證明這些性能命中。 就像這個,即使計算字符串中的每個字節,當你將它移動到內存時,這應該是隱藏的,你需要編寫這樣的代碼,如果你打算做這樣的事情,你可以考慮燒寫組合字節的指令在寫作之前寫成一個單詞,可能會也可能不會更快......取決於。
如果我有半字(strh)然后毫不奇怪,它也遭受相同的讀 - 修改 - 寫,因為ram是32位寬(加上任何ecc位,如果有的話)
0001000C str
00010007 str
0002FFFD strh
0002FFFD strh
0002FFFD strb
0002FFFD strb
當sram寬度作為一個整體被讀取並放在總線上時,處理器需要相同的時間,處理器從中提取感興趣的字節通道,因此沒有時間/時鍾成本。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.