[英]Why is writing to memory much slower than reading it?
這是一個簡單的memset
帶寬基准:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
int main()
{
unsigned long n, r, i;
unsigned char *p;
clock_t c0, c1;
double elapsed;
n = 1000 * 1000 * 1000; /* GB */
r = 100; /* repeat */
p = calloc(n, 1);
c0 = clock();
for(i = 0; i < r; ++i) {
memset(p, (int)i, n);
printf("%4d/%4ld\r", p[0], r); /* "use" the result */
fflush(stdout);
}
c1 = clock();
elapsed = (c1 - c0) / (double)CLOCKS_PER_SEC;
printf("Bandwidth = %6.3f GB/s (Giga = 10^9)\n", (double)n * r / elapsed / 1e9);
free(p);
}
在我的系統(詳情如下)中使用單個DDR3-1600內存模塊,它輸出:
帶寬= 4.751 GB / s(千兆= 10 ^ 9)
這是理論RAM速度的37%: 1.6 GHz * 8 bytes = 12.8 GB/s
另一方面,這是一個類似的“閱讀”測試:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
unsigned long do_xor(const unsigned long* p, unsigned long n)
{
unsigned long i, x = 0;
for(i = 0; i < n; ++i)
x ^= p[i];
return x;
}
int main()
{
unsigned long n, r, i;
unsigned long *p;
clock_t c0, c1;
double elapsed;
n = 1000 * 1000 * 1000; /* GB */
r = 100; /* repeat */
p = calloc(n/sizeof(unsigned long), sizeof(unsigned long));
c0 = clock();
for(i = 0; i < r; ++i) {
p[0] = do_xor(p, n / sizeof(unsigned long)); /* "use" the result */
printf("%4ld/%4ld\r", i, r);
fflush(stdout);
}
c1 = clock();
elapsed = (c1 - c0) / (double)CLOCKS_PER_SEC;
printf("Bandwidth = %6.3f GB/s (Giga = 10^9)\n", (double)n * r / elapsed / 1e9);
free(p);
}
它輸出:
帶寬= 11.516 GB / s(千兆= 10 ^ 9)
我可以接近讀取性能的理論極限,例如對大型陣列進行異或,但寫入速度要慢得多。 為什么?
OS Ubuntu 14.04 AMD64(我用gcc -O3
編譯。使用-O3 -march=native
使讀取性能稍差,但不影響memset
)
CPU Xeon E5-2630 v2
RAM單個“16GB PC3-12800奇偶REG CL11 240針DIMM”(它在盒子上說的內容)我認為擁有一個DIMM可以使性能更具可預測性。 我假設有4個DIMM, memset
將高達快4倍。
主板 Supermicro X9DRG-QF(支持4通道內存)
附加系統 :具有2x 4GB DDR3-1067 RAM的筆記本電腦:讀取和寫入都是大約5.5 GB / s,但請注意它使用2個DIMM。
使用此版本替換memset
PS會產生完全相同的性能
void *my_memset(void *s, int c, size_t n)
{
unsigned long i = 0;
for(i = 0; i < n; ++i)
((char*)s)[i] = (char)c;
return s;
}
有了你的程序,我明白了
(write) Bandwidth = 6.076 GB/s
(read) Bandwidth = 10.916 GB/s
在具有六個2GB DIMM的台式機(Core i7,x86-64,GCC 4.9,GNU libc 2.19)上。 (我手邊沒有更多細節,抱歉。)
但是, 該程序報告寫入帶寬為12.209 GB/s
:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <emmintrin.h>
static void
nt_memset(char *buf, unsigned char val, size_t n)
{
/* this will only work with aligned address and size */
assert((uintptr_t)buf % sizeof(__m128i) == 0);
assert(n % sizeof(__m128i) == 0);
__m128i xval = _mm_set_epi8(val, val, val, val,
val, val, val, val,
val, val, val, val,
val, val, val, val);
for (__m128i *p = (__m128i*)buf; p < (__m128i*)(buf + n); p++)
_mm_stream_si128(p, xval);
_mm_sfence();
}
/* same main() as your write test, except calling nt_memset instead of memset */
神奇的是_mm_stream_si128
,也就是機器指令movntdq
,它將16字節的數量寫入系統RAM, 繞過緩存 (官方術語是“ 非臨時存儲 ”)。 我覺得這很確鑿地證明了性能上的差異是所有的緩存行為。
NB glibc 2.19 確實有一個精心設計的手動優化的memset
,它使用向量指令。 但是,它不使用非臨時商店。 對於memset
,這可能是正確的事情; 通常,您在使用它之前不久就會清除內存,因此您希望它在緩存中很熱。 (我認為一個更聰明的memset
可能會切換到非臨時存儲以實現非常大的塊清除,理論上你不可能在緩存中想要所有這些,因為緩存根本不是那么大。)
Dump of assembler code for function memset:
=> 0x00007ffff7ab9420 <+0>: movd %esi,%xmm8
0x00007ffff7ab9425 <+5>: mov %rdi,%rax
0x00007ffff7ab9428 <+8>: punpcklbw %xmm8,%xmm8
0x00007ffff7ab942d <+13>: punpcklwd %xmm8,%xmm8
0x00007ffff7ab9432 <+18>: pshufd $0x0,%xmm8,%xmm8
0x00007ffff7ab9438 <+24>: cmp $0x40,%rdx
0x00007ffff7ab943c <+28>: ja 0x7ffff7ab9470 <memset+80>
0x00007ffff7ab943e <+30>: cmp $0x10,%rdx
0x00007ffff7ab9442 <+34>: jbe 0x7ffff7ab94e2 <memset+194>
0x00007ffff7ab9448 <+40>: cmp $0x20,%rdx
0x00007ffff7ab944c <+44>: movdqu %xmm8,(%rdi)
0x00007ffff7ab9451 <+49>: movdqu %xmm8,-0x10(%rdi,%rdx,1)
0x00007ffff7ab9458 <+56>: ja 0x7ffff7ab9460 <memset+64>
0x00007ffff7ab945a <+58>: repz retq
0x00007ffff7ab945c <+60>: nopl 0x0(%rax)
0x00007ffff7ab9460 <+64>: movdqu %xmm8,0x10(%rdi)
0x00007ffff7ab9466 <+70>: movdqu %xmm8,-0x20(%rdi,%rdx,1)
0x00007ffff7ab946d <+77>: retq
0x00007ffff7ab946e <+78>: xchg %ax,%ax
0x00007ffff7ab9470 <+80>: lea 0x40(%rdi),%rcx
0x00007ffff7ab9474 <+84>: movdqu %xmm8,(%rdi)
0x00007ffff7ab9479 <+89>: and $0xffffffffffffffc0,%rcx
0x00007ffff7ab947d <+93>: movdqu %xmm8,-0x10(%rdi,%rdx,1)
0x00007ffff7ab9484 <+100>: movdqu %xmm8,0x10(%rdi)
0x00007ffff7ab948a <+106>: movdqu %xmm8,-0x20(%rdi,%rdx,1)
0x00007ffff7ab9491 <+113>: movdqu %xmm8,0x20(%rdi)
0x00007ffff7ab9497 <+119>: movdqu %xmm8,-0x30(%rdi,%rdx,1)
0x00007ffff7ab949e <+126>: movdqu %xmm8,0x30(%rdi)
0x00007ffff7ab94a4 <+132>: movdqu %xmm8,-0x40(%rdi,%rdx,1)
0x00007ffff7ab94ab <+139>: add %rdi,%rdx
0x00007ffff7ab94ae <+142>: and $0xffffffffffffffc0,%rdx
0x00007ffff7ab94b2 <+146>: cmp %rdx,%rcx
0x00007ffff7ab94b5 <+149>: je 0x7ffff7ab945a <memset+58>
0x00007ffff7ab94b7 <+151>: nopw 0x0(%rax,%rax,1)
0x00007ffff7ab94c0 <+160>: movdqa %xmm8,(%rcx)
0x00007ffff7ab94c5 <+165>: movdqa %xmm8,0x10(%rcx)
0x00007ffff7ab94cb <+171>: movdqa %xmm8,0x20(%rcx)
0x00007ffff7ab94d1 <+177>: movdqa %xmm8,0x30(%rcx)
0x00007ffff7ab94d7 <+183>: add $0x40,%rcx
0x00007ffff7ab94db <+187>: cmp %rcx,%rdx
0x00007ffff7ab94de <+190>: jne 0x7ffff7ab94c0 <memset+160>
0x00007ffff7ab94e0 <+192>: repz retq
0x00007ffff7ab94e2 <+194>: movq %xmm8,%rcx
0x00007ffff7ab94e7 <+199>: test $0x18,%dl
0x00007ffff7ab94ea <+202>: jne 0x7ffff7ab950e <memset+238>
0x00007ffff7ab94ec <+204>: test $0x4,%dl
0x00007ffff7ab94ef <+207>: jne 0x7ffff7ab9507 <memset+231>
0x00007ffff7ab94f1 <+209>: test $0x1,%dl
0x00007ffff7ab94f4 <+212>: je 0x7ffff7ab94f8 <memset+216>
0x00007ffff7ab94f6 <+214>: mov %cl,(%rdi)
0x00007ffff7ab94f8 <+216>: test $0x2,%dl
0x00007ffff7ab94fb <+219>: je 0x7ffff7ab945a <memset+58>
0x00007ffff7ab9501 <+225>: mov %cx,-0x2(%rax,%rdx,1)
0x00007ffff7ab9506 <+230>: retq
0x00007ffff7ab9507 <+231>: mov %ecx,(%rdi)
0x00007ffff7ab9509 <+233>: mov %ecx,-0x4(%rdi,%rdx,1)
0x00007ffff7ab950d <+237>: retq
0x00007ffff7ab950e <+238>: mov %rcx,(%rdi)
0x00007ffff7ab9511 <+241>: mov %rcx,-0x8(%rdi,%rdx,1)
0x00007ffff7ab9516 <+246>: retq
(這是在libc.so.6
,而不是程序本身 - 試圖為memset
轉儲程序集的另一個人似乎只發現了它的PLT條目。最簡單的方法來獲取真正的memset
的匯編轉儲Unixy系統是
$ gdb ./a.out
(gdb) set env LD_BIND_NOW t
(gdb) b main
Breakpoint 1 at [address]
(gdb) r
Breakpoint 1, [address] in main ()
(gdb) disas memset
...
。)
性能的主要區別來自PC /內存區域的緩存策略。 當您從內存中讀取並且數據不在緩存中時,必須首先通過內存總線將內存提取到緩存,然后才能對數據執行任何計算。 但是,當您寫入內存時,會有不同的寫入策略。 很可能你的系統正在使用回寫緩存(或者更確切地說是“寫分配”),這意味着當你寫入不在緩存中的內存位置時,數據首先從內存中提取到緩存並最終寫入當數據從高速緩存中逐出時,返回存儲器,這意味着數據的往返和寫入時的2x總線帶寬使用。 還有直寫高速緩存策略(或“無寫入分配”),這通常意味着在寫入時高速緩存未命中時,數據不會被提取到高速緩存,並且應該使讀取和接收的數據更接近相同的性能。寫道。
差異 - 至少在我的機器上,與AMD處理器 - 是讀取程序使用矢量化操作。 對編寫程序進行反編譯會得到以下結果:
0000000000400610 <main>:
...
400628: e8 73 ff ff ff callq 4005a0 <clock@plt>
40062d: 49 89 c4 mov %rax,%r12
400630: 89 de mov %ebx,%esi
400632: ba 00 ca 9a 3b mov $0x3b9aca00,%edx
400637: 48 89 ef mov %rbp,%rdi
40063a: e8 71 ff ff ff callq 4005b0 <memset@plt>
40063f: 0f b6 55 00 movzbl 0x0(%rbp),%edx
400643: b9 64 00 00 00 mov $0x64,%ecx
400648: be 34 08 40 00 mov $0x400834,%esi
40064d: bf 01 00 00 00 mov $0x1,%edi
400652: 31 c0 xor %eax,%eax
400654: 48 83 c3 01 add $0x1,%rbx
400658: e8 a3 ff ff ff callq 400600 <__printf_chk@plt>
但這對於閱讀計划:
00000000004005d0 <main>:
....
400609: e8 62 ff ff ff callq 400570 <clock@plt>
40060e: 49 d1 ee shr %r14
400611: 48 89 44 24 18 mov %rax,0x18(%rsp)
400616: 4b 8d 04 e7 lea (%r15,%r12,8),%rax
40061a: 4b 8d 1c 36 lea (%r14,%r14,1),%rbx
40061e: 48 89 44 24 10 mov %rax,0x10(%rsp)
400623: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
400628: 4d 85 e4 test %r12,%r12
40062b: 0f 84 df 00 00 00 je 400710 <main+0x140>
400631: 49 8b 17 mov (%r15),%rdx
400634: bf 01 00 00 00 mov $0x1,%edi
400639: 48 8b 74 24 10 mov 0x10(%rsp),%rsi
40063e: 66 0f ef c0 pxor %xmm0,%xmm0
400642: 31 c9 xor %ecx,%ecx
400644: 0f 1f 40 00 nopl 0x0(%rax)
400648: 48 83 c1 01 add $0x1,%rcx
40064c: 66 0f ef 06 pxor (%rsi),%xmm0
400650: 48 83 c6 10 add $0x10,%rsi
400654: 49 39 ce cmp %rcx,%r14
400657: 77 ef ja 400648 <main+0x78>
400659: 66 0f 6f d0 movdqa %xmm0,%xmm2 ;!!!! vectorized magic
40065d: 48 01 df add %rbx,%rdi
400660: 66 0f 73 da 08 psrldq $0x8,%xmm2
400665: 66 0f ef c2 pxor %xmm2,%xmm0
400669: 66 0f 7f 04 24 movdqa %xmm0,(%rsp)
40066e: 48 8b 04 24 mov (%rsp),%rax
400672: 48 31 d0 xor %rdx,%rax
400675: 48 39 dd cmp %rbx,%rbp
400678: 74 04 je 40067e <main+0xae>
40067a: 49 33 04 ff xor (%r15,%rdi,8),%rax
40067e: 4c 89 ea mov %r13,%rdx
400681: 49 89 07 mov %rax,(%r15)
400684: b9 64 00 00 00 mov $0x64,%ecx
400689: be 04 0a 40 00 mov $0x400a04,%esi
400695: e8 26 ff ff ff callq 4005c0 <__printf_chk@plt>
40068e: bf 01 00 00 00 mov $0x1,%edi
400693: 31 c0 xor %eax,%eax
另外,請注意,您的“自制” memset
實際上已經優化到對memset
的調用:
00000000004007b0 <my_memset>:
4007b0: 48 85 d2 test %rdx,%rdx
4007b3: 74 1b je 4007d0 <my_memset+0x20>
4007b5: 48 83 ec 08 sub $0x8,%rsp
4007b9: 40 0f be f6 movsbl %sil,%esi
4007bd: e8 ee fd ff ff callq 4005b0 <memset@plt>
4007c2: 48 83 c4 08 add $0x8,%rsp
4007c6: c3 retq
4007c7: 66 0f 1f 84 00 00 00 nopw 0x0(%rax,%rax,1)
4007ce: 00 00
4007d0: 48 89 f8 mov %rdi,%rax
4007d3: c3 retq
4007d4: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4007db: 00 00 00
4007de: 66 90 xchg %ax,%ax
我找不到關於memset
是否使用向量化操作的任何參考, memset@plt
的反匯編在這里是無益的:
00000000004005b0 <memset@plt>:
4005b0: ff 25 72 0a 20 00 jmpq *0x200a72(%rip) # 601028 <_GLOBAL_OFFSET_TABLE_+0x28>
4005b6: 68 02 00 00 00 pushq $0x2
4005bb: e9 c0 ff ff ff jmpq 400580 <_init+0x20>
這個問題表明,由於memset
旨在處理每個案例,因此可能缺少一些優化。
這個人肯定相信你需要使用自己的匯編程序memset
來利用SIMD指令。 這個問題也是如此 。
我將在黑暗中拍攝並猜測它沒有使用SIMD操作,因為它無法判斷它是否會在一個矢量化操作大小的倍數上運行,或者有一些對齊相關問題。
但是,通過使用cachegrind
進行檢查,我們可以確認這不是緩存效率的問題。 寫程序產生以下內容:
==19593== D refs: 6,312,618,768 (80,386 rd + 6,312,538,382 wr)
==19593== D1 misses: 1,578,132,439 ( 5,350 rd + 1,578,127,089 wr)
==19593== LLd misses: 1,578,131,849 ( 4,806 rd + 1,578,127,043 wr)
==19593== D1 miss rate: 24.9% ( 6.6% + 24.9% )
==19593== LLd miss rate: 24.9% ( 5.9% + 24.9% )
==19593==
==19593== LL refs: 1,578,133,467 ( 6,378 rd + 1,578,127,089 wr)
==19593== LL misses: 1,578,132,871 ( 5,828 rd + 1,578,127,043 wr) <<
==19593== LL miss rate: 9.0% ( 0.0% + 24.9% )
並且讀取程序產生:
==19682== D refs: 6,312,618,618 (6,250,080,336 rd + 62,538,282 wr)
==19682== D1 misses: 1,578,132,331 (1,562,505,046 rd + 15,627,285 wr)
==19682== LLd misses: 1,578,131,740 (1,562,504,500 rd + 15,627,240 wr)
==19682== D1 miss rate: 24.9% ( 24.9% + 24.9% )
==19682== LLd miss rate: 24.9% ( 24.9% + 24.9% )
==19682==
==19682== LL refs: 1,578,133,357 (1,562,506,072 rd + 15,627,285 wr)
==19682== LL misses: 1,578,132,760 (1,562,505,520 rd + 15,627,240 wr) <<
==19682== LL miss rate: 4.1% ( 4.1% + 24.9% )
雖然讀取程序具有較低的LL未命中率,因為它執行更多讀取(每次XOR
操作額外讀取),但未命中總數是相同的。 所以無論問題是什么,都不存在。
緩存和位置幾乎可以肯定地解釋了您所看到的大部分效果。
寫入時沒有任何緩存或位置,除非您需要非確定性系統。 大多數寫入時間都是以數據一直到達存儲介質所需的時間來衡量的(無論是硬盤驅動器還是內存芯片),而讀取可以來自任何數量的緩存層,這些緩存層比存儲介質。
它可能就是它如何(整個系統)執行。 讀取速度更快似乎是具有廣泛相對吞吐量性能的常見趨勢 。 快速分析列出的DDR3英特爾和DDR2圖表, 作為幾個選擇案例(寫/讀)% ;
一些性能最佳的DDR3芯片的寫入速率約為讀取吞吐量的60-70%。 但是,有一些內存模塊(即Golden Empire CL11-13-13 D3-2666)下降到只有~30%寫入。
與讀取相比,性能最佳的DDR2芯片似乎僅具有約50%的寫入吞吐量。 但也有一些特別糟糕的競爭者(即OCZ OCZ21066NEW_BT1G)降至約20%。
雖然這可能無法解釋報告的~40%寫入/讀取的原因,但由於使用的基准代碼和設置可能不同( 注釋含糊不清 ),這絕對是一個因素。 (我會運行一些現有的基准程序,看看這些數字是否與問題中發布的代碼一致。)
更新:
我從鏈接的站點下載了內存查找表並在Excel中進行了處理。 雖然它仍然顯示了廣泛的值,但它比上面的原始回復要小得多,后者僅查看頂部讀取的內存芯片和一些從圖表中選擇的“有趣”條目。 我不確定為什么這些差異,特別是在上面列出的可怕競爭者中,不存在於次要名單中。
然而,即使在新數字下,差異仍然廣泛地在讀取性能的50%-100%(中位數65,平均值65)之間。 請注意,僅僅因為芯片在寫入/讀取比率方面“100%”有效並不意味着它總體上更好。只是它在兩個操作之間更加均衡。
這是我的工作假設。 如果正確,它解釋了為什么寫入比讀取慢兩倍:
即使memset
僅寫入虛擬內存,忽略其先前的內容,在硬件級別,計算機也無法對DRAM進行純寫入:它將DRAM的內容讀入緩存,在那里修改它們然后將它們寫回DRAM。 因此,在硬件層面, memset
同時進行讀寫(即使前者看起來沒用)! 因此大約兩倍的速度差異。
因為讀取你只是脈沖地址線並讀出感測線上的核心狀態。 回寫周期在數據傳遞到CPU之后發生,因此不會減慢速度。 另一方面,要寫入,必須首先執行偽讀取以重置核心,然后執行寫入循環。
(以防萬一它不明顯,這個答案是詼諧的 - 描述為什么寫入比在舊的核心內存盒上讀取要慢。)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.