首页 文章

为什么写入内存比读取内存慢得多?

提问于
浏览
48

这是一个简单的 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 至强E5-2630 v2

RAM 单个"16GB PC3-12800 Parity REG CL11 240-Pin DIMM"(它在盒子上说的内容)我认为使用单个DIMM可以使性能更具可预测性 . 我假设使用4个DIMM, memset 将快4倍 .

Motherboard Supermicro X9DRG-QF(支持4通道内存)

Additional system :具有2x 4GB DDR3-1067 RAM的笔记本电脑:读取和写入都是大约5.5 GB / s,但请注意它使用2个DIMM .

P.S. 用此版本替换 memset 会产生完全相同的性能

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

7 回答

  • 6

    缓存和位置几乎可以肯定地解释了您所看到的大部分效果 .

    写入时没有任何缓存或位置,除非您需要非确定性系统 . 大多数写入时间都是以数据一直到达存储介质所需的时间来衡量的(无论是硬盘驱动器还是内存芯片),而读取可以来自任何数量的缓存层,这些缓存层比存储介质 .

  • 16

    有了你的程序,我明白了

    (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,绕过缓存(官方术语是“non-temporal store”) . 我认为这非常有说服力地证明了性能差异与缓存行为有关 .

    注: 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 ,而不是程序本身 - 试图转储程序集的另一个人似乎只是找到了它的PLT条目 . 在Unixy系统上获取真正的 memset 的汇编转储的最简单方法是

    $ 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
    ...
    

    . )

  • 9

    性能的主要区别来自PC /内存区域的缓存策略 . 当您从内存中读取并且数据不在缓存中时,必须首先通过内存总线将内存提取到缓存,然后才能对数据执行任何计算 . 但是,当您写入内存时,会有不同的写入策略 . 很可能你的系统正在使用回写缓存(或者更确切地说是“写分配”),这意味着当你写入不在缓存中的内存位置时,数据首先从内存中提取到缓存并最终写入当数据从高速缓存中逐出时,返回存储器,这意味着数据的往返和写入时的2x总线带宽使用 . 还有直写高速缓存策略(或“无写入分配”),这通常意味着在写入时高速缓存未命中时,数据不会被提取到高速缓存,并且应该使读取和接收的数据更接近相同的性能 . 写道 .

  • 4

    差异 - 至少在我的机器上,与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
    

    另请注意,您的"homegrown" 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>
    

    This question表明,由于 memset 旨在处理每个案例,因此可能缺少一些优化 .

    This guy绝对相信您需要使用自己的汇编程序 memset 来利用SIMD指令 . This question does, too .

    我将在黑暗中拍摄,并猜测它没有使用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 操作额外读取),但未命中的总数是相同的 . 所以无论问题是什么,都不存在 .

  • 28

    它可能就是它如何(整个系统)执行 . 读取速度更快appears to be a common trend具有广泛的相对吞吐量性能 . 快速分析列出的DDR3英特尔和DDR2图表,作为几个选择案例(写/读)%;

    一些性能最佳的DDR3芯片的写入速率约为读取吞吐量的60-70% . 但是,有一些内存模块(即Golden Empire CL11-13-13 D3-2666)下降到只有~30%写入 .

    与读取相比,性能最佳的DDR2芯片似乎仅具有约50%的写入吞吐量 . 但也有一些特别糟糕的竞争者(即OCZ OCZ21066NEW_BT1G)降至约20% .

    虽然这可能无法解释报告的~40%写入/读取的原因,但由于使用的基准代码和设置可能不同(notes are vague),这绝对是一个因素 . (我会运行一些现有的基准程序,看看这些数字是否与问题中发布的代码一致 . )


    更新:

    我从链接的站点下载了内存查找表并在Excel中进行了处理 . 虽然它仍然显示了大范围的值,但它比上面的原始回复要小得多,后者仅查看顶部读取的内存芯片和图表中的一些选定的"interesting"条目 . 我不确定为什么这些差异,特别是在上面列出的可怕竞争者中,不存在于次要名单中 .

    然而,即使在新数字下,差异仍然广泛地在读取性能的50%-100%(中位数65,平均值65)之间 . 请注意,仅仅因为芯片在写入/读取比率方面“100%”有效并不意味着它总体上更好 . 只是它在两个操作之间更加均衡 .

  • 43

    这是我的工作假设 . 如果正确,它解释了为什么写入比读取慢两倍:

    即使 memset 仅写入虚拟内存,忽略其先前的内容,在硬件级别,计算机也无法对DRAM进行纯写入:它将DRAM的内容读入缓存,在那里修改它们然后将它们写回DRAM . 因此,在硬件层面, memset 同时进行读写(即使前者似乎无用)!因此大约两倍的速度差异 .

  • 2

    因为读取你只是脉冲地址线并读出感测线上的核心状态 . 回写周期在数据传递到CPU之后发生,因此不会减慢速度 . 另一方面,要写入,必须首先执行伪读取以重置核心,然后执行写入循环 .

    (以防万一它不明显,这个答案是诙谐的 - 描述为什么写入比在旧的核心内存盒上读取要慢 . )

相关问题