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

55

这是一个简单的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

另一方面,以下是类似的“读取”测试:

#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)

我可以接近读取性能的理论极限,例如对大数组进行异或运算,但写入似乎要慢得多。为什么?

操作系统 Ubuntu 14.04 AMD64 (我使用gcc -O3编译。使用 -O3 -march=native会稍微降低读取性能,但不会影响 memset

CPU Xeon E5-2630 v2

内存 单个“16GB PC3-12800 Parity REG CL11 240-Pin DIMM”(盒子上写着这个)。我认为单个DIMM可以使性能更加可预测。我假设使用4个DIMM,memset将会更快,最多可达4倍。

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

其他系统:一台配备有2个DDR3-1067 RAM的笔记本电脑:读和写都约为5.5 GB/s,但请注意它使用了2个DIMM。

附注:使用此版本替换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;
}

11
在你的基准测试中,printf("%4d/%4ld\r", p[0], r); 很可能是你正在计时的内容,而不是其他任何东西。I/O 操作很慢。 - Retired Ninja
5
不!在一个运行时间为20秒的程序中,printf被调用了101次。 - MWB
5
在你发布的代码中,应该调用它100次。它不应该在你正在进行基准测试的代码部分中出现,这没有任何意义。 - Retired Ninja
2
我在我的系统上尝试了一下,在循环中使用和不使用printf。差异比我预期的要小(运行了3次)。使用printf,我得到了9.644、9.667和9.629,而不使用printf,我得到了9.740、9.614和9.653。 - some
2
我的2010年老款MacBook在没有优化的情况下报告1.937 GB/s,在使用发布的代码进行优化后,速度为173010.381 GB/s,且未经修改 :-) 很可能是memset写入了一个缓存行,该缓存行首先从RAM读取到缓存中以进行修改,然后被刷新,因此每个缓存行都被读取+写入而不仅仅是读取。剩余的差异很可能是由于在非连续位置读/写造成的。PowerPC有清除缓存行的指令,这将有所帮助。 - gnasher729
显示剩余22条评论
7个回答

49
使用你的程序,我能够得到
(write) Bandwidth =  6.076 GB/s
(read)  Bandwidth = 10.916 GB/s

在一台桌面电脑上(Core i7,x86-64,GCC 4.9,GNU libc 2.19),使用六个2GB DIMM。 (很抱歉我手头没有更多的细节。)
然而,程序报告写入带宽为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 中,而不是程序本身。另一个尝试转储 memset 汇编的人似乎只找到了它的 PLT 条目。在类 Unix 系统上获取真正的 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
...

.)


啊,我想我对memset的理解是错误的,感谢您发布正确的反汇编代码。而且知道在gdb中的这个技巧真是太好了! - Patrick Collins
movnt存储可以为大型内存设置提供更好的写入带宽的主要原因是它们是弱排序的。当写入新的缓存行时,它们可以跳过读取所有权步骤,因为它们不能保证与彼此或与普通存储一起全局可见。在具有“快速字符串操作”的CPU(Intel IvB及更高版本)上,“rep stos”使用略微弱排序的存储以获得相同的加速,但不会绕过缓存。据我理解文档,操作结束时有一个存储栅栏,因此不要将标志作为memset / cpy的一部分存储。 - Peter Cordes
@PeterCordes 如果我理解你的评论正确的话,即使缓存行将被完全覆盖,CPU核心也会读取它的内容?有没有办法在其他写入指令中强制执行这种“弱”行为?(我的意思是,有没有一种方法可以在不先读取内存的情况下写入数据,并将数据保留在缓存中?) - Will
@Will:要让其他存储器以这种方式工作,您必须将数据写入到一个WC(不可缓存的写组合)而不是普通的WB内存区域中,并使用MTRRs或PAT进行设置。在大多数操作系统下,您通常无法轻松地从用户空间分配此类内存,这使得有效读取变得困难。有关NT存储与常规存储的更多信息,请参见Enhanced REP MOVSB for memcpy。是的,普通的强序存储总是在将数据提交到处于修改状态的L1d高速缓存之前执行RFO(读取所有权),而不仅仅是使其他高速缓存失效并进入DRAM。 - Peter Cordes

30
主要的性能差异来自于PC/内存区域的缓存策略。当你从内存读取数据,且该数据不在缓存中时,需要通过内存总线先将该数据从内存中获取到缓存中,然后才能对该数据进行任何计算。但是,当你写入数据到内存时,有不同的写入策略。大多数系统使用写回缓存(或更准确地说是“写分配”),这意味着当你向一个不在缓存中的内存位置写入数据时,该数据首先被从内存中获取到缓存中,然后在数据从缓存中移除时再写回到内存中,这意味着数据的往返和2倍的总线带宽使用量用于写操作。还有一种写穿透缓存策略(或“无写分配”),通常意味着在写入时缓存未命中,数据不会被获取到缓存中,这应该使读取和写入的性能更接近。

谢谢您确认了我之前的猜测(我在大约30分钟前发布了它)!除非有人说服我它事实上是不准确的,否则我将接受它。 - MWB
在某些平台上,您实际上可以控制每个分配的缓存策略,而写入性能是其中之一的原因。 - JarkkoL
传统的架构会在某个时刻将所有脏数据写回内存。现在,许多平台都试图通过额外的缓存控制功能来提高性能。例如,像Cavium Octeon这样的平台提供了特殊的缓存控制策略,如DWB(不写回)选项,以避免写回L2缓存数据。由于这一点,可以避免不必要的L2数据写回到内存中。 - Karthik Balaguru

16

至少在我的电脑上,使用 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指令,而这个问题也持有同样的看法。

我猜测memset没有使用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%  )

虽然读取程序因为执行了更多的读操作(每个XOR操作需要一次额外的读取)有较低的LL缺失率,但总的缺失数是一样的。所以无论问题是什么,它都不在那里。


你是否也看到了带宽有两倍的差异?你能发布你的数字和RAM配置吗? - MWB
2
这个家伙肯定是相信的...他的缓冲区小了244000倍,并适合于各种高速缓存。 - MWB
你的memset几乎肯定是向量化的; 一些更聪明的实现将在启动向量化版本之前运行一个小循环,直到对齐。 我猜你在Linux上,可能使用glibc,所以这里是它的memset。(通过与GOT的调整或在GDB中进行一些stepi,您应该能够自己找到实现。) - saagarjha

9
缓存和局部性几乎可以解释您看到的大部分效果。
除非您想要一个不确定的系统,否则写入时没有任何缓存或局部性。大多数写入时间都是指数据到达存储介质(无论是硬盘还是内存芯片)所需的时间,而读取可以来自比存储介质更快的任意数量的缓存层。

1 GB 的数组比任何缓存大小都要大得多(这就是我选择它的原因)。在 do_xor 第二次运行时,任何先前缓存的值都将被清除。此外,如果这种情况确实存在,缓存可能解释了读取速度比 DRAM->Cache 链路快的现象。但这并不能解释写入速度慢的问题。 - MWB
5
希望您清楚地认识到,即使没有1GB的高速缓存,仍然可以看到缓存效应。 - Robert Harvey
1
+1 -- 我愿意打赌预取与此有关;它不会帮助写入,但会帮助读取。我也愿意打赌GCC不太愿意重新排列写入而不是读取。 - Patrick Collins
在x86上,普通存储(不是movnt)是强有序的。写入一个冷缓存行会触发读取所有权。据我所知,CPU确实会从DRAM(或更低级别的缓存)中读取以填充缓存行。对于具有强有序内存(如x86)的系统来说,写入比读取更困难,但原因并非您提供的那样。存储可以被缓冲并在同一线程执行的负载之后变为全局可见。(MFENCE是StoreLoad屏障...)AMD出于简单起见使用写通缓存,而英特尔则使用写回缓存以获得更好的性能。 - Peter Cordes
在实践中,使用适合于L1的缓冲区重复写入循环(例如memset)比使用较大的缓冲区更快是绝对正确的。其中一部分原因是已经处于M状态(MESI协议)的行不需要驱逐任何其他行(如果被驱逐的行处于M状态并且必须首先写入L2,则可能会停顿,特别是如果L2然后驱逐了修改的行等,直到DRAM)。但另一部分原因是避免在缓存行已经处于E或M状态时进行读取以获取所有权。movnt和Fast String rep movsb弱序存储可以避免RFO。 - Peter Cordes

6

可能这就是整个系统的表现方式。阅读速度更快似乎是一种常见趋势,具有相对吞吐量性能的广泛范围。在DDR3 Intel和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%”高效并不意味着它整体表现更好..只是两个操作之间更加平稳。


他们是否安装了1个DIMM或多个DIMM尚不清楚。我相信这可能会产生非常显著的差异。我的测试是“纯粹”的,因为我只有1个DIMM。 - MWB
@MaxB,这并不是非常清楚,但它确实显示了一系列值。那么我的建议是看看其他基准测试程序是否在特定机器上产生类似的值;如果是这样,在不同的硬件上发布的基准测试也是如此。 - user2864740

4
这是我的工作假设。如果正确,它可以解释为什么写操作大约比读操作慢两倍:
虽然memset只向虚拟内存中写入数据,忽略其先前的内容,但在硬件层面上,计算机无法对DRAM进行纯粹的写操作:它会将DRAM的内容读取到缓存中,在那里进行修改,然后将其写回DRAM。因此,在硬件层面上,memset既进行了读取又进行了写入(即使前者似乎毫无用处)!因此,速度差异大约为两倍。

1
您可以使用弱序存储(movnt或Intel IvB及更高版本的rep stos / rep movs“快速字符串操作”)来避免此读取所有权。很遗憾,没有方便的方法可以进行弱序存储(除了在最近的Intel CPU上使用memset / memcpy),而不绕过缓存。我在其他答案中留下了类似的评论:正常写入触发读取的主要原因是x86的强序内存模型。限制系统到一个DIMM或不应该成为这个因素。 - Peter Cordes
我预计其他架构(如ARM)在不需要额外努力的情况下就可以以完整的DRAM带宽进行写入,因为不能保证存储将按程序顺序对其他线程可见。例如,对于热缓存行的存储可能会立即发生(或者至少在确保没有先前指令出错或是错误预测分支之后),但对于冷缓存行的存储可能只会被缓冲,而其他核心无法看到该值,直到完全重写冷缓存行并刷新存储缓冲区。 - Peter Cordes

2

为了读取数据,只需脉冲地址线并在感应线上读取核心状态即可。写回周期发生在数据传递给CPU之后,因此不会减慢速度。另一方面,要进行写操作,必须先执行一个虚拟读取以重置核心,然后执行写入周期。

(如果不明显的话,这个答案是半开玩笑的——描述为什么旧的磁芯存储器盒中写入比读取慢。)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接