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

Why is writing to memory much slower than reading it?

提问人:MWB 提问时间:9/14/2014 最后编辑:MWB 更新时间:9/15/2014 访问量:13154

问:

下面是一个简单的带宽基准: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 (Giga = 10^9)

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

操作系统Ubuntu 14.04 AMD64(我用.使用会使读取性能稍差,但不影响gcc -O3-O3 -march=nativememset)

中央处理器至强 E5-2630 v2

公羊单个“16GB PC3-12800 Parity REG CL11 240-Pin DIMM”(包装盒上写着) 我认为拥有单个 DIMM 可以使性能更可预测。我假设使用 4 个 DIMM,速度将提高 4 倍memset

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

附加系统:配备 2 个 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;
}
C 高性能 内存 硬件

评论

11赞 Retired Ninja 9/14/2014
printf("%4d/%4ld\r", p[0], r);在你的基准测试中,意味着你很可能是在计时,而不是其他任何事情。I/O 速度较慢。
5赞 MWB 9/14/2014
@RetiredNinja 不! 在运行 20 秒的程序中被调用 101 次printf
5赞 Retired Ninja 9/14/2014
在您发布的代码中,它应该被调用 100 次。没有理由让它出现在你正在基准测试的代码部分。
2赞 some 9/14/2014
我在我的系统上尝试了它,无论循环中是否有 printf。差异比我预期的要小(运行 3 次)。有了,我得到了 9.644、9.667 和 9.629,没有我得到了 9.740、9.614 和 9.653
2赞 gnasher729 9/14/2014
我的 2010 年旧 MacBook 报告 1.937 GB/s 未经优化,173010.381 GB/s 使用发布的代码进行优化,未经修改:-)最有可能的是,memset 写入缓存行,该缓存行首先从 RAM 读取到缓存以进行修改,然后刷新,因此每个缓存行都是读取 + 写入的,而不仅仅是读取。其余的差异可能是由于在不连续的位置进行读/写。PowerPC 有清除缓存行的指令,这会有所帮助。

答:

6赞 user2864740 9/14/2014 #1

它可能就是它(系统作为一个整体)的表现。读取速度更快似乎是相对吞吐量性能范围广泛的常见趋势。在对 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%”有效并不意味着它总体上更好。只是在两个操作之间更加平衡。

评论

0赞 MWB 9/14/2014
目前尚不清楚他们是否安装了 1 个 DIMM 或多个 DIMM。我相信这可以产生非常重大的影响。我的测试是“纯粹的”,因为我只有 1 个 DIMM。
0赞 user2864740 9/14/2014
@MaxB 它根本不清楚,但它确实显示了广泛的值。这就是为什么我的建议是查看其他基准测试程序是否在特定机器上产生类似的值;如果是这样,如果发布的基准测试在不同的硬件上也效仿。
9赞 Robert Harvey 9/14/2014 #2

缓存和局部性几乎可以肯定地解释了你所看到的大多数影响。

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

评论

0赞 MWB 9/14/2014
1 GB 阵列比任何缓存大小都大得多(这就是我选择它的原因)。到第二次运行时,任何以前缓存的值都将被清除。此外,缓存可以解释读取速度比 DRAM->Cache 链接快(如果是这样的话)。它并不能解释写作速度较慢的原因。do_xor
5赞 Robert Harvey 9/14/2014
我希望不言而喻的是,您不需要 1GB 缓存即可仍然看到缓存效果。
1赞 Patrick Collins 9/14/2014
+1 -- 我敢打赌,预取与此有关;它不会帮助那些写作,但它会帮助阅读。我也愿意打赌,GCC 不愿意对写入进行重新排序,而不是对读取进行重新排序。
0赞 Peter Cordes 9/29/2015
在 x86 上,普通存储 (not ) 是强排序的。写入冷缓存行会触发为所有权读取。据我了解,CPU确实会从DRAM中读取(或较低级别的缓存)以填充缓存行。对于具有强有序内存的系统(如 x86),写入比读取更难,但不是因为您给出的原因。允许对存储进行缓冲,并在同一线程完成加载后全局可见。(MFENCE 是 StoreLoad 屏障...为了简单起见,AMD 确实使用直写式缓存,但 Intel 使用回写式缓存以获得更好的性能。movnt
0赞 Peter Cordes 9/29/2015
在实践中,使用适合 L1 的缓冲区重复只写循环(如 memset)比使用更大的缓冲区更快,这绝对是正确的。部分原因是已经处于 M 状态(MESI)的行不需要驱逐任何其他行(如果被驱逐的行处于 M 状态并且必须首先写入 L2,则可能会停止,特别是如果 L2 随后逐出修改后的行,等等,直到 DRAM)。但另一部分是避免在缓存行已经处于 E 或 M 状态时读取所有权。 和 Fast String rep movsb 弱有序存储避免 RFO。movnt
4赞 MWB 9/14/2014 #3

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

即使只写入虚拟内存,忽略其先前的内容,在硬件级别,计算机也无法对 DRAM 进行纯粹的写入:它将 DRAM 的内容读入缓存,在那里修改它们,然后将它们写回 DRAM。因此,在硬件层面,既可以读,也可以写(即使前者似乎毫无用处)!因此,速度差大约是原来的两倍。memsetmemset

评论

1赞 Peter Cordes 9/29/2015
您可以使用弱排序存储(或英特尔 IvB 及更高版本/“快速字符串操作”)来避免这种所有权读取。糟糕的是,没有一种方便的方法可以在不绕过缓存的情况下进行弱排序存储(除了最近 Intel CPU 上的 memset/memcpy)。我对其他一些答案留下了类似的评论:正常写入触发读取的主要原因是 x86 的强有序内存模型。将您的系统限制为一个 DIMM 不应该是一个因素。movntrep stosrep movs
0赞 Peter Cordes 9/29/2015
我希望其他一些架构,如 ARM,可以毫不费力地以全 DRAM 带宽写入,因为不能保证存储按程序顺序对其他线程可见。例如,存储到热缓存行可能会立即发生(或者至少在确保没有先前的指令可以出错或成为错误预测的分支之后),但存储到冷缓存行可能只是被缓冲,而其他内核无法看到该值,直到冷缓存行被完全重写并刷新存储缓冲区。
30赞 JarkkoL 9/14/2014 #4

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

评论

0赞 MWB 9/15/2014
感谢您确认我之前的猜测(我在~30 分钟前发布)!我会接受它,直到/除非有人说服我它实际上不准确。
0赞 JarkkoL 9/15/2014
在某些平台上,您实际上可以控制每个分配的缓存策略,写入性能是原因之一。
0赞 Karthik Balaguru 10/2/2014
传统架构会在某个时间点将所有脏数据写回内存。如今,许多平台都试图通过额外的缓存控制功能来提高性能。例如,像 Cavium Octeon 这样的平台提供了特殊的缓存控制策略,如 DWB(不写回)选项,用于不写回 L2 缓存数据。因此,可以避免不必要的 L2 数据回写到内存中。
16赞 Patrick Collins 9/14/2014 #5

区别 - 至少在我的机器上,使用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

另外,请注意,您的“自制”实际上已优化为对以下命令的调用:memsetmemset

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

我找不到任何关于是否使用矢量化操作的参考资料,这里的反汇编是没有帮助的:memsetmemset@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

这家伙似乎肯定相信您需要推出自己的汇编程序才能利用 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%  )

虽然读取程序具有较低的 LL 未命中率,因为它执行更多的读取(每个操作额外读取),但未命中总数是相同的。所以不管问题是什么,它都不存在。XOR

评论

0赞 MWB 9/14/2014
您是否也看到了 2 倍的带宽差异?你能发布你的号码和RAM配置吗?
2赞 MWB 9/14/2014
This guy definitely seems convinced ...他的缓冲区小 244000 倍,适合各种缓存。
0赞 saagarjha 4/12/2020
几乎可以肯定,你的记忆集在某种程度上是矢量化的;一些更智能的实现在启动到矢量化版本之前会运行一个小循环,直到对齐。我猜你在 Linux 上,可能使用 glibc,所以这是它的内存集。(稍微摆弄一下 GOT,或者在 GDB 中玩几个,您应该能够自己找到实现。stepi
49赞 zwol 9/15/2014 #6

通过你的程序,我得到了

(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 */

魔术在于,又名机器指令,它将 16 字节的数量写入系统 RAM,绕过缓存(官方术语是“非临时存储”)。我认为这非常确凿地表明,性能差异完全缓存行为有关。_mm_stream_si128movntdq

N.B. glibc 2.19 确实有一个精心手工优化的,它利用了矢量指令。但是,它使用非时态存储。这可能是正确的选择;通常,您会在使用内存前不久清除内存,因此您希望它在缓存中处于热状态。(我想更聪明的人可能会切换到非临时存储以获得非常大的块清除,理论上你不可能希望所有这些都在缓存中,因为缓存根本没有那么memsetmemsetmemset

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   

(这是在 ,而不是程序本身 -- 试图转储程序集的其他人似乎只找到了它的 PLT 条目。在 Unixy 系统上获取真实程序集转储的最简单方法是libc.so.6memsetmemset

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

.)

评论

0赞 Patrick Collins 9/17/2014
啊,我想我一定是错了,谢谢你发布正确的反汇编。很高兴知道 gdb 中的这个技巧!memset
0赞 Peter Cordes 9/29/2015
存储可以为大型内存集提供更好的写入带宽的主要原因是它们是弱排序的。在写入新的缓存行时,它们可以跳过“为所有权读取”步骤,因为不能保证它们在彼此之间或相对于正常存储的顺序上全局可见。在具有“快速字符串操作”的 CPU(Intel IvB 及更高版本)上,使用某种程度较弱的有序存储来获得相同的加速,但不会绕过缓存。据我了解文档,操作结束时有一个存储围栏,所以不要将标志存储为 memset/cpy 的一部分。movntrep stos
0赞 Will 1/14/2020
@PeterCordes如果我理解您的评论,即使缓存行将被完全覆盖,CPU 内核是否仍在读取缓存行?有没有办法在其他写入指令中强制这种“弱”行为?(我的意思是,有没有办法在不先读取内存的情况下写入内存,并将数据保存在缓存中?
0赞 Peter Cordes 1/15/2020
@Will:要使其他存储以这种方式工作,您必须写入 WC(不可缓存写入组合)而不是使用 MTRR 或 PAT 设置的普通 WB 的内存区域。在大多数操作系统下,您通常无法以这种方式从用户空间轻松分配内存,这使得高效读取变得困难。另请参阅 memcpy 的增强型 REP MOVSB,了解有关 NT 商店与常规商店的更多信息。是的,正常的强排序存储总是在将数据提交到处于“已修改”状态的 L1d 缓存之前执行 RFO(读取所有权),而不是只是使其他缓存失效并转到 DRAM。
2赞 Hot Licks 9/15/2014 #7

因为要读取,您只需脉冲地址线并读出检测线上的内核状态。回写周期发生在数据传送到 CPU 之后,因此不会减慢速度。另一方面,要进行写入,必须首先执行假读取以重置内核,然后执行写入周期。

(以防万一,这个答案是开玩笑的——描述为什么写入比在旧核心内存盒上读取慢。