为什么写内存要比读内存慢得多?
这是一个简单的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(Giga = 10 ^ 9)
这是理论RAM速度的37%: 1.6 GHz * 8 bytes = 12.8 GB/s
另一方面,这里有一个类似的“阅读”testing:
#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(Giga = 10 ^ 9)
我可以接近读取性能的理论极限,比如对大数组进行异或运算,但写入速度要慢得多。 为什么?
操作系统 Ubuntu 14.04 AMD64(我使用gcc -O3
编译,使用-O3 -march=native
会使读取性能稍差,但不影响memset
)
CPU至强E5-2630 v2
内存一个单一的“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。
用这个版本replacememset
结果是完全一样的性能
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
在一个桌面上(Core i7,x86-64,GCC 4.9,GNU libc 2.19)机器与六个2GB的DIMMs。 (我没有比这个更详细的信息,对不起。)
但是, 该程序报告的写入带宽为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, 绕过caching (官方术语是“ 非临时存储 ”)。 我认为这非常确定地表明,性能差异是关于caching行为。
NB glibc 2.19 确实有一个精心devise的手动优化的memset
,使用向量指令。 但是,它不使用非临时存储。 这可能是memset
的正确的事情; 一般来说,在使用之前你要清除内存,所以你希望它在caching中很热。 (我认为,一个更聪明的memset
可能会切换到非临时存储,因为理论上你不可能想要所有这些在caching中,因为caching根本不是那么大。)
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
的其他人似乎只能find它的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 /内存区域的caching策略。 当你从内存中读取数据并且不在caching中时,必须首先通过内存总线将内存提取到caching中,然后才能对数据执行任何计算。 但是,当您写入内存时,有不同的写入策略。 很可能你的系统正在使用写回caching(或者更准确地说是“写分配”),这意味着当你写入一个不在caching中的内存位置时,数据首先从内存中获取到caching中并最终被写入当数据从高速caching中被逐出时,回到内存,这意味着写数据时的数据和2x总线带宽使用率的往返。 还有直写caching策略(或“无写入分配”),这通常意味着在写入caching未命中时,数据不会被提取到caching中,并且对于读取和读取都应该具有更接近相同的性能写道。
至less在我的机器上,与AMD处理器的区别在于,读取程序正在使用vector化操作。 反编译这两个产生这个写作程序:
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
被devise来处理每一种情况,它可能会缺less一些优化。
这个人肯定相信你需要推出你自己的汇编器memset
来利用SIMD指令。 这个问题也是 。
我会在黑暗中拍摄一下,猜测它不是在使用SIMD操作,因为它不能确定它是否在一个向量化操作的大小的倍数上操作,或者有一些alignment相关的问题。
但是,我们可以通过检查cachegrind
来确认这不是caching效率问题。 写程序产生以下内容:
==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
操作需要额外读取一次),但总失败次数相同。 所以无论什么问题,都不在那里。
caching和地方几乎可以解释你所看到的大部分效果。
除非你想要一个非确定性的系统,否则写入时没有任何caching或局部性。 大多数写入时间是以数据到达存储介质(无论是硬盘驱动器还是存储器芯片)所花费的时间来衡量的,而读取可以来自任何速度快于存储介质。
它可能就是它(系统一体)如何执行的。 读取更快似乎是一个普遍的趋势 ,具有广泛的相对吞吐量性能。 在对DDR3 Intel和DDR2图表的快速分析中, 作为(写/读)%的一些select案例 ;
一些性能最好的DDR3芯片的写入速度约为读取吞吐量的60-70%。 但是,有一些内存模块(即Golden Empire CL11-13-13 D3-2666)只能下降〜30%的写入。
与读取相比,性能最高的DDR2芯片似乎只有约50%的写入吞吐量。 但也有一些明显不好的竞争者(即OCZ OCZ21066NEW_BT1G)下降到20%左右。
虽然这可能无法解释报告的40%写入/读取的原因,因为所使用的基准代码和设置可能不同( 注释模糊 ),但这绝对是一个因素。 (我会运行一些现有的基准程序,看看这些数字是否与问题中发布的代码一致。)
更新:
我从链接的站点下载了内存查找表,并在Excel中处理它。 虽然它仍然显示了广泛的价值,但它比原来的答复要less得多,因为上面的答案仅仅是从顶层读取的内存芯片和一些从图表中select的“有趣的”条目。 我不确定为什么这些差异,特别是在上面挑出的可怕的竞争者中,没有出现在二级名单中。
然而,即使在新的数字下,读取性能的差异仍然在50%-100%(中值65,平均65)之间。 请注意,仅仅因为芯片在写入/读取比率上“100%”效率并不意味着总体上更好,只是在两个操作之间更加平坦。
这是我的工作假设。 如果正确的话,它解释了为什么写比读取慢两倍:
尽pipememset
只写入虚拟内存,忽略了以前的内容,但是在硬件层面,计算机不能对DRAM进行纯粹的写操作:将DRAM的内容读入caching,在那里修改它们,然后写回DRAM。 因此,在硬件层面上, memset
既可以读写,也可以写(即使前者似乎没用)! 因此,差不多是两倍的速度。
因为读你只需简单的脉冲地址线,读出感测线上的核心状态。 写回周期发生在数据传送到CPU之后,因此不会减慢速度。 另一方面,为了编写你必须首先执行一个假读取来重置内核,然后执行写周期。
(以防万一,这个答案不是很明显,这个答案很难说 – 描述为什么写入比在旧的核心存储器上读取要慢。)