增强的REP MOVSB用于memcpy

101
我希望使用增强的REP MOVSB (ERMSB) 来为自定义的memcpy获取高带宽。
ERMSB是在Ivy Bridge微架构中引入的。如果您不知道ERMSB是什么,请参阅Intel优化手册中的“增强的REP MOVSB和STOSB操作(ERMSB)”部分。
我所知道的唯一直接做法是使用内联汇编。我从https://groups.google.com/forum/#!topic/gnu.gcc.help/-Bmlm_EG_fE中获得了以下函数。
static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

当我使用这个函数时,其带宽比memcpy低得多。__movsb在我的i7-6700HQ(Skylake)系统,Ubuntu 16.10,DDR4@2400 MHz双通道32 GB,GCC 6.2中达到15 GB/s,而memcpy则为26 GB/s。
为什么使用REP MOVSB的带宽较低?我该如何改进它?
以下是我用来测试的代码。
//gcc -O3 -march=native -fopenmp foo.c
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stddef.h>
#include <omp.h>
#include <x86intrin.h>

static inline void *__movsb(void *d, const void *s, size_t n) {
  asm volatile ("rep movsb"
                : "=D" (d),
                  "=S" (s),
                  "=c" (n)
                : "0" (d),
                  "1" (s),
                  "2" (n)
                : "memory");
  return d;
}

int main(void) {
  int n = 1<<30;

  //char *a = malloc(n), *b = malloc(n);

  char *a = _mm_malloc(n,4096), *b = _mm_malloc(n,4096);
  memset(a,2,n), memset(b,1,n);

  __movsb(b,a,n);
  printf("%d\n", memcmp(b,a,n));

  double dtime;
  
  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) __movsb(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);

  dtime = -omp_get_wtime();
  for(int i=0; i<10; i++) memcpy(b,a,n);
  dtime += omp_get_wtime();
  printf("dtime %f, %.2f GB/s\n", dtime, 2.0*10*1E-9*n/dtime);  
}

我对rep movsb感兴趣的原因是基于以下评论:
请注意,在Ivybridge和Haswell上,由于缓冲区太大而无法适应MLC,您可以使用rep movsb击败movntdqa;movntdqa会在LLC中产生RFO,而rep movsb不会…… 与在Ivybridge和Haswell上流式传输到内存时相比,rep movsb比movntdqa快得多(但请注意,它在Ivybridge之前很慢!) 这个memcpy实现中缺少/次优的内容是什么?
这是我在tinymembnech上同一系统的测试结果。
 C copy backwards                                     :   7910.6 MB/s (1.4%)
 C copy backwards (32 byte blocks)                    :   7696.6 MB/s (0.9%)
 C copy backwards (64 byte blocks)                    :   7679.5 MB/s (0.7%)
 C copy                                               :   8811.0 MB/s (1.2%)
 C copy prefetched (32 bytes step)                    :   9328.4 MB/s (0.5%)
 C copy prefetched (64 bytes step)                    :   9355.1 MB/s (0.6%)
 C 2-pass copy                                        :   6474.3 MB/s (1.3%)
 C 2-pass copy prefetched (32 bytes step)             :   7072.9 MB/s (1.2%)
 C 2-pass copy prefetched (64 bytes step)             :   7065.2 MB/s (0.8%)
 C fill                                               :  14426.0 MB/s (1.5%)
 C fill (shuffle within 16 byte blocks)               :  14198.0 MB/s (1.1%)
 C fill (shuffle within 32 byte blocks)               :  14422.0 MB/s (1.7%)
 C fill (shuffle within 64 byte blocks)               :  14178.3 MB/s (1.0%)
 ---
 standard memcpy                                      :  12784.4 MB/s (1.9%)
 standard memset                                      :  30630.3 MB/s (1.1%)
 ---
 MOVSB copy                                           :   8712.0 MB/s (2.0%)
 MOVSD copy                                           :   8712.7 MB/s (1.9%)
 SSE2 copy                                            :   8952.2 MB/s (0.7%)
 SSE2 nontemporal copy                                :  12538.2 MB/s (0.8%)
 SSE2 copy prefetched (32 bytes step)                 :   9553.6 MB/s (0.8%)
 SSE2 copy prefetched (64 bytes step)                 :   9458.5 MB/s (0.5%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  13103.2 MB/s (0.7%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  13179.1 MB/s (0.9%)
 SSE2 2-pass copy                                     :   7250.6 MB/s (0.7%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7437.8 MB/s (0.6%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7498.2 MB/s (0.9%)
 SSE2 2-pass nontemporal copy                         :   3776.6 MB/s (1.4%)
 SSE2 fill                                            :  14701.3 MB/s (1.6%)
 SSE2 nontemporal fill                                :  34188.3 MB/s (0.8%)

请注意,在我的系统上,SSE2 copy prefetchedMOVSB copy更快。
在我的原始测试中,我没有禁用Turbo。我禁用了Turbo并重新进行了测试,似乎没有太大的区别。但是,更改电源管理确实会有很大的影响。
当我这样做时:
sudo cpufreq-set -r -g performance

我有时会看到超过20 GB/s的速度,使用rep movsb

使用

sudo cpufreq-set -r -g powersave

我看到的最佳速度约为17GB/s。但是memcpy似乎对电源管理不敏感。

我使用 turbostat 检查了频率(启用和禁用SpeedStep),对于空闲、1核负载和4核负载,分别使用 performancepowersave。我运行了英特尔的 MKL 密集矩阵乘法来创建负载,并使用 OMP_SET_NUM_THREADS 设置线程数。以下是结果表格(以 GHz 为单位)。

              SpeedStep     idle      1 core    4 core
powersave     OFF           0.8       2.6       2.6
performance   OFF           2.6       2.6       2.6
powersave     ON            0.8       3.5       3.1
performance   ON            3.5       3.5       3.1

这表明即使关闭SpeedStep,使用powersave时CPU仍会降频至空闲频率0.8 GHz。只有在关闭SpeedStep的情况下使用performance时,CPU才会以恒定频率运行。
我使用了例如sudo cpufreq-set -r performance(因为cpufreq-set的结果很奇怪)来更改电源设置。这会重新启用Turbo,所以我必须在之后禁用Turbo。

3
@Ped7g,我并不期望它比memcpy更好。我希望它的表现和memcpy差不多。我使用gdb调试了memcpy函数,发现它进入一个主循环,并使用了rep movsb指令。所以这似乎就是memcpy在某些情况下使用的方式。 - Z boson
3
@KerrekSB,是的,它在第3.7.6节“增强的REP MOVSB和STOSB操作(ERMSB)”中。 - Z boson
3
优化手册建议使用ERMSB可以提供更小的代码大小和吞吐量比传统的REP-MOV/STO更好,但是“使用ERMSB实现memcpy可能无法达到与使用256位或128位AVX替代方案相同的吞吐量水平,这取决于长度和对齐因素。” 我的理解是,ERMSB针对以前可能已经使用rep指令的情况进行了优化,但它并不旨在与现代高吞吐量的替代方案竞争。 - Kerrek SB
5
我的 glibc 的 memcpy() 使用了 AVX NT 存储方式。而 NT 存储和 ERMSB 都以写合并的方式进行,因此不应需要 RFO(读取请求)。然而,在我的机器上进行基准测试显示,我的 memcpy() 和我的 ERMSB 都仅达到总带宽的 2/3,就像你的 memcpy()(但不是_你的_ ERMSB)一样。因此,显然在某个地方有额外的总线事务,并且非常像 RFO。 - Iwillnotexist Idonotexist
3
值得注意的是,快速字符串性能在一些场景中非常重要,例如Linux内核方法(如read()write())将数据复制到用户空间时:内核不能(不会)使用任何SIMD寄存器或SIMD代码,因此为了进行快速的memcpy操作,它必须使用64位的load/store指令,或者更近期它将使用rep movsbrep rmovd如果在该架构上检测到其速度很快,则可以获得大块移动的许多好处,而无需明确使用xmmymm寄存器。 - BeeOnRope
显示剩余46条评论
6个回答

139

这个话题与我的心思息息相关,最近我进行了一些调查,因此我将从几个角度来看待它:历史、一些技术注释(主要是学术方面)、我的测试结果以及最终尝试回答你的实际问题,即何时何地使用rep movsb会更有意义。

部分原因是为了分享结果——如果你能运行Tinymembench并分享结果以及你的CPU和RAM配置细节,那就太好了。特别是如果你拥有4通道设置、Ivy Bridge盒子、服务器盒子等。

历史和官方建议

快速字符串复制指令的性能历史有点像阶梯式的 - 即,停滞不前的表现期交替出现着大幅升级,使它们达到或甚至比竞争方法更快的水平。例如,在 Nehalem 中性能有了飞跃(主要针对启动开销),在 Ivy Bridge 中又有了提升(主要针对大型复制的总吞吐量)。你可以在此帖子中找到一位英特尔工程师关于实现rep movs指令困难的十年前的见解。

例如,在 Ivy Bridge 推出之前的指南中,典型的建议是避免使用它们或非常谨慎地使用它们1

目前的(截至 2016 年 6 月)指南有各种令人困惑且有些不一致的建议,例如2

特定变量的实现是根据数据布局、对齐和计数器(ECX)值在执行时选择的。例如,带有REP前缀的MOVSB/STOSB应该在计数器值小于或等于三时使用以获得最佳性能。 所以对于长度在3个字节或以下的拷贝操作?你一开始就不需要使用rep前缀,因为声明启动延迟约为9个周期,用简单的DWORD或QWORD mov加上一些位掩码来屏蔽未使用的字节肯定更好(或者如果你知道大小恰好为三,则可以使用2个明确的字节、word移动).
他们接着说: 字符串MOVE/STORE指令具有多个数据粒度。为了有效地移动数据,较大的数据粒度更可取。这意味着通过将任意计数器值分解为多个双字和单字节移动来实现小于或等于3的计数值,可以实现更高的效率。
这在当前使用 ERMSB 的硬件上似乎是错误的,因为对于大型复制,rep movsb 至少与 movdmovq 变体一样快,甚至更快。

总的来说,当前指南中的第 3.7.5 节包含了合理和过时建议的混合。这在英特尔手册中很常见,因为它们是针对每种架构进行增量更新的(即使在当前手册中也声称覆盖了近二十年的架构),而旧的部分通常没有更新以替换或使不适用于当前架构的建议有条件地生效。

然后他们继续在第 3.7.6 节中明确介绍了 ERMSB。

我不会详尽介绍剩余的建议,但我将在下面的“为什么使用它”中总结好的部分。

指南中的其他重要声明是,在 Haswell 上,rep movsb 已经增强为在内部使用 256 位操作。

技术考虑

这只是从一个实现的角度快速总结了 rep 指令的优缺点。

rep movs 的优点

When a rep movs指令被发出时,CPU会知道整个已知大小的块将被传输。这可以帮助它以无法通过离散指令进行优化的方式进行操作,例如:
  • 当它知道整个缓存行将被覆盖时,避免RFO请求。
  • 立即准确地发出预取请求。硬件预取在检测到类似memcpy的模式时表现良好,但它仍需要几次读取才能启动,并且会“过度预取”许多超出复制区域末端的缓存行。rep movsb精确地知道区域大小并且可以准确地预取。
2. 显然,在单个rep movs中,商店之间没有排序保证,这可以帮助简化一致性流量和简化块移动的其他方面,而不是必须遵守相当严格的内存排序的简单mov指令。
3. 原则上,rep movs指令可以利用各种未在ISA中公开的架构技巧。例如,体系结构可能具有ISA未公开的更宽的内部数据路径,并且rep movs可以在内部使用它们。

缺点

  1. rep movsb必须实现特定的语义,这可能比底层软件要求更强。特别是,memcpy禁止重叠区域,因此可能忽略该可能性,但rep movsb允许它们,并且必须产生预期结果。在当前实现中,这主要影响启动开销,但可能不会影响大块吞吐量。同样,rep movsb必须支持字节粒度的复制,即使您实际上正在使用它来复制某些2的大幂次方的大块。

  2. 如果使用rep movsb,软件可能具有关于对齐、复制大小和可能的别名的信息,无法与硬件通信。编译器通常可以确定内存块的对齐方式6,因此可以避免rep movs在每次调用时必须执行的大部分启动工作。

测试结果

以下是在我的i7-6700HQ 2.6 GHz上使用tinymembench进行多种不同复制方法的测试结果(很遗憾,我也有相同的CPU,所以我们没有得到新数据点...):
 C copy backwards                                     :   8284.8 MB/s (0.3%)
 C copy backwards (32 byte blocks)                    :   8273.9 MB/s (0.4%)
 C copy backwards (64 byte blocks)                    :   8321.9 MB/s (0.8%)
 C copy                                               :   8863.1 MB/s (0.3%)
 C copy prefetched (32 bytes step)                    :   8900.8 MB/s (0.3%)
 C copy prefetched (64 bytes step)                    :   8817.5 MB/s (0.5%)
 C 2-pass copy                                        :   6492.3 MB/s (0.3%)
 C 2-pass copy prefetched (32 bytes step)             :   6516.0 MB/s (2.4%)
 C 2-pass copy prefetched (64 bytes step)             :   6520.5 MB/s (1.2%)
 ---
 standard memcpy                                      :  12169.8 MB/s (3.4%)
 standard memset                                      :  23479.9 MB/s (4.2%)
 ---
 MOVSB copy                                           :  10197.7 MB/s (1.6%)
 MOVSD copy                                           :  10177.6 MB/s (1.6%)
 SSE2 copy                                            :   8973.3 MB/s (2.5%)
 SSE2 nontemporal copy                                :  12924.0 MB/s (1.7%)
 SSE2 copy prefetched (32 bytes step)                 :   9014.2 MB/s (2.7%)
 SSE2 copy prefetched (64 bytes step)                 :   8964.5 MB/s (2.3%)
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11777.2 MB/s (5.6%)
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11826.8 MB/s (3.2%)
 SSE2 2-pass copy                                     :   7529.5 MB/s (1.8%)
 SSE2 2-pass copy prefetched (32 bytes step)          :   7122.5 MB/s (1.0%)
 SSE2 2-pass copy prefetched (64 bytes step)          :   7214.9 MB/s (1.4%)
 SSE2 2-pass nontemporal copy                         :   4987.0 MB/s

一些关键点:
  • rep movs方法比其他非“非暂态”方法快,比每次复制8个字节的“C”方法快得多。
  • “非暂态”方法比rep movs方法快约26%-但这比您报告的差距小得多(26 GB / s vs 15 GB / s =〜73%)。
  • 如果您不使用非暂态存储,则使用从C中复制的8字节副本与128位宽SSE加载/存储几乎一样好。这是因为良好的复制循环可以生成足够的内存压力来饱和带宽(例如,2.6 GHz * 1个存储器/周期* 8字节= 26 GB / s用于存储)。
  • tinymembench中没有显式的256位算法(除了可能是“标准”的memcpy),但由于上述说明,这可能并不重要。
  • 非暂态存储方法的吞吐量增加约为1.45倍,非常接近预期的1.5倍,即NT消除3个传输中的1个(即1个读取,1个写入用于NT vs 2个读取,1个写入)。 rep movs方法处于中间位置。
  • 相对较低的内存延迟和适度的2通道带宽组合意味着这个特定芯片碰巧能够从单线程饱和其内存带宽,这会显着改变行为。
  • rep movsd在此芯片上似乎使用与rep movsb相同的技巧。这很有趣,因为ERMSB仅明确针对movsb,而早期使用ERMSB进行的早期测试显示movsbmovsd执行得更快。这主要是学术性的,因为movsbmovsd更通用。

Haswell

看着评论中iwillnotexist友好提供的Haswell结果,我们可以看到相同的普遍趋势(提取最相关的结果):

 C copy                                               :   6777.8 MB/s (0.4%)
 standard memcpy                                      :  10487.3 MB/s (0.5%)
 MOVSB copy                                           :   9393.9 MB/s (0.2%)
 MOVSD copy                                           :   9155.0 MB/s (1.6%)
 SSE2 copy                                            :   6780.5 MB/s (0.4%)
 SSE2 nontemporal copy                                :  10688.2 MB/s (0.3%)
rep movsb的方法仍然比非暂态memcpy慢,但只比Skylake测试中的大约26%慢14%。NT技术的优势超过它们的时间表兄弟约57%,甚至比带宽降低的理论收益更多。

何时应该使用rep movs

最终回答您的实际问题:何时或为什么要使用它?它借鉴了上面的内容并引入了一些新想法。不幸的是,没有简单的答案:您必须权衡各种因素,包括一些您可能无法准确知道的因素,例如未来的发展。

请注意,rep movsb的替代方法可能是经过优化的libc memcpy(包括编译器内联的副本),也可能是手动编写的memcpy版本。以下某些好处仅适用于与这些替代方法之一进行比较(例如,“简单性”有助于对抗手动编写的版本,但不适用于内置的memcpy),但有些则适用于两者。

可用指令的限制

在某些环境中,有限制某些指令或使用某些寄存器的要求。例如,在Linux内核中,通常不允许使用SSE/AVX或FP寄存器。因此,大多数优化的memcpy变体无法使用,因为它们依赖于SSE或AVX寄存器,并且在x86上使用基于64位mov的普通复制。对于这些平台,使用rep movsb允许获得大部分优化memcpy的性能,而不会违反SIMD代码的限制。
更一般的例子可能是必须针对许多硬件代数的代码,而且不使用特定于硬件的调度(例如,使用cpuid)。在这种情况下,你可能被迫只使用较旧的指令集,这排除了任何AVX等。在这里,rep movsb可能是一个好的方法,因为它允许“隐藏”的访问更广泛的加载和存储,而不使用新的指令。如果你针对的是先前的ERMSB硬件,则必须查看rep movsb的性能是否可接受,尽管...。

未来的保障

rep movsb的一个好处是,它可以在未来的架构改进中(理论上)发挥作用,而不需要源代码更改,这是显式移动所不能做到的。例如,当引入256位数据路径时,rep movsb能够利用它们(如Intel所声称)而无需对软件进行任何更改。使用128位移动的软件(在Haswell之前是最优的)必须进行修改和重新编译。
因此,它既是一种软件维护的好处(无需更改源代码),也是现有二进制文件的好处(无需部署新二进制文件即可利用改进)。
这取决于您的维护模型(例如,在实践中部署新二进制文件的频率)以及非常难以判断这些指令在未来可能有多快。至少Intel正在引导用户朝着这个方向发展,承诺在未来保持“合理”的性能(15.3.3.6):

REP MOVSB和REP STOSB将在未来的处理器上继续表现得相当不错。

与后续工作重叠

这个好处在普通的memcpy基准测试中不会显示出来,因为它没有后续的工作可以重叠,所以这个好处的大小必须在实际情况下仔细测量。充分利用可能需要重新组织围绕memcpy的代码。
英特尔在他们的优化手册(第11.16.3.4节)中指出了这个好处,并用他们的话说:
当计数至少为1000字节或更多时,使用增强的REP MOVSB / STOSB可以提供另一个优势,以摊销非消耗代码的成本。该启发式方法可以使用Cnt = 4096和memset()作为示例来理解:
• 使用256位SIMD实现的memset()需要发出/执行128个32字节存储操作,然后才能使非消耗指令序列到达退休。
• 使用ECX = 4096的增强REP STOSB实例被硬件解码为长微操作流,但作为一条指令退役。在memset()的结果可以被消耗之前,必须完成许多store_data操作。由于存储数据操作的完成与程序顺序退休解耦,因此如果非消耗序列不争夺存储缓冲区资源,则可以处理大量的非消耗代码流,实际上是免费的。
英特尔表示,在执行了rep movsb后,一些uops已经发出,但是当大量存储仍在飞行中且整个rep movsb尚未退役时,后续指令的uops可以通过乱序机制更快地取得进展,而如果该代码在复制循环之后,则无法做到这一点。
明确的加载和存储循环中的uops必须按程序顺序分别退役。这必须发生以为后续uops腾出ROB中的空间。
关于非常长的微码指令(例如rep movsb)的工作方式,似乎没有太多详细信息。我们不知道微码分支如何请求来自微码序列器的不同uop流,或者uop如何退役。如果单个uop不必分别退役,那么整个指令可能只占用ROB中的一个插槽?
当前端向OoO机器提供的uop缓存中看到rep movsb指令时,它会激活Microcode Sequencer ROM (MS-ROM)将微码uops发送到队列中,以供issue/rename阶段使用。当rep movsb正在发出时,可能不可能有其他uops混合在其中并发出/执行8,但是在某些复制尚未执行之前,随后的指令可以被获取/解码并在最后一个rep movsb uop执行后立即发出。只有在至少一部分后续代码不依赖于memcpy结果时,这才有用(这并不罕见)。
现在,这个好处的大小是有限制的:最多可以执行N条指令(实际上是uops),超过缓慢的rep movsb指令后,您将会停顿,其中N是ROB size。当前ROB大小约为200(Haswell上为192,Skylake上为224),这是对于IPC为1的后续代码的免费工作的最大受益约为200个周期。在200个周期内,您可以以10 GB/s的速度复制约800字节的数据,因此对于这样大小的复制,您可能会获得接近复制成本的免费工作(以某种方式使复制免费)。
然而,随着复制大小变得更大,这个相对重要性迅速降低(例如,如果您要复制80 KB,则免费工作仅占复制成本的1%)。但对于适度大小的复制来说,它仍然非常有趣。
复制循环也不会完全阻止后续指令的执行。英特尔没有详细说明受益的大小,或者哪种复制或周围代码受益最大。(热或冷目标或源,高ILP或低ILP高延迟代码之后)。

代码大小

执行的代码大小(几个字节)与典型优化的memcpy例程相比微不足道。如果性能受到i-cache(包括uop缓存)未命中的限制,减少代码大小可能会带来好处。
同样,我们可以根据复制的大小来限定这种好处的数量。我不会真正地数值计算出来,但直觉上,将动态代码大小减少B字节最多可以节省C * B次缓存未命中,其中C是某个常数。每次对memcpy的调用都会产生一次缓存未命中成本(或好处),但高吞吐量的优势随着复制的字节数扩大而扩大。因此,对于大型传输,更高的吞吐量将主导缓存效应。
同样,在普通基准测试中,这并不会出现,因为整个循环无疑将适合于uop缓存。您需要进行实际的原地测试以评估此效果。

架构特定的优化

您报告说,在您的硬件上,rep movsb比平台的memcpy慢得多。然而,即使在早期硬件(如Ivy Bridge)上也有相反结果的报告。

这完全是有道理的,因为似乎字符串移动操作会定期得到关注 - 但并非每一代都如此,因此在已经更新到最新架构的情况下,它可能会更快或者至少与其他优势相结合而获胜(在这种情况下,基于其他优势它可能会获胜),只是在随后的硬件上落后。

引用 Andy Glew,在实现P6上的这些操作之后,他应该对此有所了解:

快速处理字符串的一个重大弱点是使用微码[...]每一代微码都会失调,变得越来越慢,直到有人解决为止。就像库存拷贝一样容易失调。我想可能有一个错过的机会是在128位载入和存储可用时使用它们等等。

In that case, it can be seen as just another "platform specific" optimization to apply in the typical every-trick-in-the-book `memcpy` routines you find in standard libraries and JIT compilers: but only for use on architectures where it is better. For JIT or AOT-compiled stuff this is easy, but for statically compiled binaries this does require platform specific dispatch, but that often already exists (sometimes implemented at link time), or the `mtune` argument can be used to make a static decision.
Simplicity
Even on Skylake, where it seems like it has fallen behind the absolute fastest non-temporal techniques, it is still faster than most approaches and is very simple. This means less time in validation, fewer mystery bugs, less time tuning and updating a monster `memcpy` implementation (or, conversely, less dependency on the whims of the standard library implementors if you rely on that).
Latency Bound Platforms
Memory throughput bound algorithms can actually be operating in two main overall regimes: DRAM bandwidth bound or concurrency/latency bound.
第一种模式可能是您熟悉的:DRAM子系统具有一定的理论带宽,您可以根据通道数、数据速率/宽度和频率相对容易地计算出来。例如,我的DDR4-2133系统有2个通道,最大带宽为2.133 * 8 * 2 = 34.1 GB/s,与ARK上报道的相同。
您不会从DRAM中获得更高的速率(通常由于各种效率低下而略低),即使在插座上的所有内核中增加了这种速率(即,对于单插座系统,它是全局限制)。
另一个限制是由每个内核实际发出到内存子系统的并发请求数量所限制的。想象一下,如果一个内核一次只能有1个64字节缓存行的请求正在进行 - 当请求完成时,您可以发出另一个请求。假设还有非常快的50纳秒的内存延迟。那么尽管有大量的34.1 GB/s DRAM带宽,您实际上只会获得64字节/50 ns = 1.28 GB/s,即不到最大带宽的4%。
实际上,处理器核心可以同时发出多个请求,但数量是有限制的。通常认为每个核心在L1和内存层次结构之间只有10个线路填充缓冲区,在L2和DRAM之间大约有16个或更多的填充缓冲区。预取与其他资源竞争,但至少有助于减少有效延迟。要了解更多详情,请查看Dr. Bandwidth在该主题上撰写的任何一篇优秀文章,主要在Intel论坛上。

然而,大多数最近的CPU受到这个因素的限制,而不是RAM带宽。通常每个核心可以实现12-20 GB/s的速度,而RAM带宽可能超过50 GB/s(在4通道系统上)。只有一些最近的第二代“客户端”核心,它们似乎具有更好的非核心部分,也许有更多的线路缓冲区可以单个核心达到DRAM极限,而我们的Skylake芯片似乎就是其中之一。

现在当然,英特尔设计具有50 GB/s DRAM带宽的系统是有原因的,而每个核心只能维持不到20 GB/s的并发限制:前者的限制是套接字范围内的,而后者是每个核心的范围。因此,在8核系统上,每个核心可以推动20 GB/s的请求,此时它们将再次受到DRAM限制。
为什么我要这样说呢?因为最好的memcpy实现通常取决于您所操作的模式。一旦您受到DRAM BW限制(我们的芯片显然是,但大多数单核心芯片不是),使用非暂态写入变得非常重要,因为它可以节省通常浪费1/3带宽的读取所有权。您可以在上面的测试结果中看到这一点:不使用NT存储的memcpy实现失去了1/3的带宽。
如果您的并发受限,情况会变得相等甚至有时会反转。您有DRAM带宽可以使用,所以NT存储不起作用,甚至可能会损害性能,因为它们可能会增加延迟,因为线路缓冲区的移交时间可能比预取将RFO行带入LLC(甚至L2)然后在LLC中完成存储以实现更低的延迟的情况更长。最后,服务器未核心通常比客户端更慢的NT存储(和高带宽),这加重了这种效应。
因此,在其他平台上,您可能会发现NT存储不那么有用(至少在关注单线程性能时),也许rep movsb在某些情况下会胜出(如果它获得了最佳效果)。
实际上,这最后一项需要进行大多数测试。我知道NT存储在大多数架构(包括当前的服务器架构)的单线程测试中失去了其明显的优势,但我不知道rep movsb相对表现如何...
参考资料

comp.arch investigation对比rep movsb及其替代方案。有关分支预测的许多好笔记,以及我经常建议用于小块的方法的实现:使用重叠的第一个和/或最后一个读/写,而不是尝试仅写入所需字节数(例如,将所有从9到16个字节的复制实现为两个8字节的复制,这些复制可能在最多7个字节上重叠)。


1 可能意图是将其限制在例如代码大小非常重要的情况下。

2 请参见第3.7.5节:REP前缀和数据移动。

3 关键是要注意,这仅适用于单个指令中的各种存储器:一旦完成,存储器块仍然与先前和后续存储器有序。因此,代码可以看到rep movs中的存储器彼此之间无序,但不会与先前或后续存储器无序(通常需要后一保证)。只有当您将复制目标的末尾用作同步标志而不是单独的存储时,才会出现问题。

4 请注意,非暂态离散存储器也避免了大多数排序要求,尽管在实践中,rep movs具有更多的自由度,因为WC / NT存储器还存在一些排序约束。

5 在32位时代的后期,许多芯片都有64位数据通路(例如,支持具备对64位double类型的FPU支持)。如今,“削弱”型号的芯片,例如Pentium或Celeron品牌已经禁用了AVX,但假定rep movs微码仍然可以使用256b的加载/存储。

6 例如,由于语言对齐规则、对齐属性或运算符、别名规则或其他在编译时确定的信息。在对齐的情况下,即使无法确定精确的对齐方式,它们也可能至少能够将对齐检查提升出循环或以其他方式消除冗余检查。

7 我假设“标准”memcpy选择了非暂态方法,这对于这个缓冲区的大小来说是非常可能的。

8这并不明显,因为生成的uop流可能会被rep movsb垄断分派,然后它看起来非常像显式mov情况。然而,似乎并不是这样工作的-随后指令的uops可以与微编码的rep movsb的uops混合。

9即那些可以发出大量独立的内存请求并因此饱和可用的DRAM到核心带宽的指令,其中memcpy将是一个典型的例子(与纯粹的延迟绑定负载相对,例如指针跟踪)。


4
这里是BeeOnRope的结果;文件包含系统和编译器信息。它支持ERMS,但结果表明在这个系统上并不那么有竞争力;这解释了我在寻找一个胜利的测试时遇到的困难。另外,你介意在你的答案中添加一条评论吗?tinymembench只进行64位对齐的复制和填充。虽然这完全适用于这里提出的问题,但它严格来说只是现实世界应用程序的典型用例的一个子集。 - Nominal Animal
3
@MaximMasiutin - 分析分支预测的讨论可能值得在SO上单独发起一个问题,但简短的答案是,最近芯片所使用的确切技术并未披露,但您可能会看到类似于Intel上的TAGEAMD上的perceptons。更一般地说,我建议您完全阅读Agner的第1、2、3篇指南。 - BeeOnRope
3
@PeterCordes - 是的,我似乎在movsdmovsb上存在不一致性,在某些地方声称它们在erms平台上具有相同的性能,但是在上面提到_早期使用ERMSB的较早架构的测试显示, movsbmovsd执行得快得多_。这已经很明确了,我肯定看到了数据,但我在这个帖子中找不到它。它可能来自RWT上这这个或这这个大型帖子中的一个,或者可能来自英特尔手册中的例子。 - BeeOnRope
2
例如,英特尔手册中有“图3-4。长度最多为2KB的Memcpy性能比较”,其中显示Ivy Bridge上的'rep movsd'(加上最后三个字节的'trailing movsb')在256字节之前明显比'movsb'差得多,此后斜率似乎相同。这里有一些Ivy Bridge结果(http://users.atw.hu/instlatx64/GenuineIntel00306A9_IvyBridge_InstLatX64.txt),显示'rep movsd'比'rep movsb'慢约3%,但也许这是测量误差之内,即使不是很大。 - BeeOnRope
2
值得注意的是,从27G周期到30G周期的变化,对于多16G指令而言,意味着这些指令的IPC为16/3=5.33,因此肯定存在一些重叠(但并不清楚它是否比显式复制循环中获得的重叠更好,正如英特尔所声称的那样)。 - BeeOnRope
显示剩余56条评论

20

增强型REP MOVSB(Ivy Bridge及更高版本)

Ivy Bridge微架构(2012年和2013年发布的处理器)引入了增强型REP MOVSB(ERMSB)。我们仍然需要检查相应的位。ERMS旨在允许我们使用rep movsb快速复制内存。

后续处理器的最便宜版本——2017年发布的Kaby Lake Celeron和Pentium,没有AVX可用于快速内存复制,但仍具有增强型REP MOVSB。而一些在2018年及以后发布的基于移动和低功耗架构的英特尔处理器,都可以通过REP MOVSB每个CPU周期复制约两倍的字节,比之前的微架构代际更高效。

冰湖微架构之前的增强型REP MOVSB(ERMSB)如果块大小至少为256字节,则仅比AVX复制或通用寄存器复制更快。对于小于64字节的块,其速度要慢得多,因为ERMSB中存在高启动时间——约35个时钟周期。FSRM功能旨在使128字节以下的块也能快速复制。

请参阅《英特尔优化手册》,第3.7.6节“增强型REP MOVSB和STOSB操作(ERMSB)”http://www.intel.com/content/dam/www/public/us/en/documents/manuals/64-ia-32-architectures-optimization-manual.pdf(适用于尚未具有FSRM的处理器):

  • 启动成本为35个时钟周期;
  • 源地址和目标地址都必须对齐到16字节边界;
  • 源区域不应与目标区域重叠;
  • 长度必须是64的倍数才能产生更高的性能;
  • 方向必须向前(CLD)。

正如我之前所说,当长度至少为256字节时,REP MOVSB(在FSRM之前的处理器上)开始胜过其他方法,但要看到与AVX复制相比的明显优势,长度必须超过2048字节。此外,应注意的是,仅使用AVX(256位寄存器)或AVX-512(512位寄存器)进行内存复制有时可能会带来严重后果,如AVX/SSE转换惩罚或降低Turbo频率。因此,REP MOVSB比AVX更安全地复制内存。

关于REP MOVSB与AVX复制的对齐效果,英特尔手册提供了以下信息:

  • 如果源缓冲区没有对齐,相对于128位AVX,ERMSB实现的影响类似;
  • 如果目标��冲区没有对齐,相对于16字节对齐的场景,ERMSB实现的效果可能会降低25%,而128位AVX实现的内存复制仅可能降低5%。

我在64位Intel Core i5-6600上进行了测试,并将REP MOVSB memcpy()与简单的MOV RAX,[SRC]; MOV [DST] RAX实现进行了比较当数据适合L1缓存时:

REP MOVSB内存复制

 - 1622400000 data blocks of  32 bytes took 17.9337 seconds to copy;  2760.8205 MB/s
 - 1622400000 data blocks of  64 bytes took 17.8364 seconds to copy;  5551.7463 MB/s
 - 811200000 data blocks of  128 bytes took 10.8098 seconds to copy;  9160.5659 MB/s
 - 405600000 data blocks of  256 bytes took  5.8616 seconds to copy; 16893.5527 MB/s
 - 202800000 data blocks of  512 bytes took  3.9315 seconds to copy; 25187.2976 MB/s
 - 101400000 data blocks of 1024 bytes took  2.1648 seconds to copy; 45743.4214 MB/s
 - 50700000 data blocks of  2048 bytes took  1.5301 seconds to copy; 64717.0642 MB/s
 - 25350000 data blocks of  4096 bytes took  1.3346 seconds to copy; 74198.4030 MB/s
 - 12675000 data blocks of  8192 bytes took  1.1069 seconds to copy; 89456.2119 MB/s
 - 6337500 data blocks of  16384 bytes took  1.1120 seconds to copy; 89053.2094 MB/s

MOV RAX...内存复制

 - 1622400000 data blocks of  32 bytes took  7.3536 seconds to copy;  6733.0256 MB/s
 - 1622400000 data blocks of  64 bytes took 10.7727 seconds to copy;  9192.1090 MB/s
 - 811200000 data blocks of  128 bytes took  8.9408 seconds to copy; 11075.4480 MB/s
 - 405600000 data blocks of  256 bytes took  8.4956 seconds to copy; 11655.8805 MB/s
 - 202800000 data blocks of  512 bytes took  9.1032 seconds to copy; 10877.8248 MB/s
 - 101400000 data blocks of 1024 bytes took  8.2539 seconds to copy; 11997.1185 MB/s
 - 50700000 data blocks of  2048 bytes took  7.7909 seconds to copy; 12710.1252 MB/s
 - 25350000 data blocks of  4096 bytes took  7.5992 seconds to copy; 13030.7062 MB/s
 - 12675000 data blocks of  8192 bytes took  7.4679 seconds to copy; 13259.9384 MB/s
即使在128位块上,REP MOVSB(在FSRM之前的处理器上)比仅循环(未展开)中的简单MOV RAX复制慢。 ERMSB实现只从256字节块开始优于MOV RAX循环。Ice Lake微架构在2019年9月推出了Fast Short REP MOV(FSRM)。该功能可以通过CPUID位进行测试。它旨在使小于或等于128个字节的字符串也快速,但实际上,在64个字节之前的字符串使用rep movsb仍然比例如简单的64位寄存器复制要慢。此外,FSRM仅在64位下实现,而不是在32位下实现。至少在我的i7-1065G7 CPU上,rep movsb仅适用于64位以下的小字符串,但在32位字符串上,rep movsb必须至少为4KB才能开始优于其他方法。对于大块,以前的体系结构(Nehalem及更高版本,不包括Ivy Bridge)的REP MOVSD / MOVSQ(但不包括REP MOVSB / MOVSW)实现相对较快,但没有足够大以超过L1缓存。 Intel优化手册(2.5.6 REP String Enhancement)给出了与Nehalem微架构相关的以下信息 - 2009年和2010年发布的Intel Core i5,i7和Xeon处理器以及包括Sandy Bridge在内的后来的微架构,直到2013年制造。根据ECX的大小,MOVSB的延迟为9个周期。否则,具有ECX> 9的REP MOVSB具有50个周期的启动成本。小字符串(ECX介于4和9之间):在Intel手册中没有官方信息,可能超过9个周期但少于50个周期;长字符串(ECX> 9):50个周期的启动成本。Intel优化手册(2.5.6 REP String Enhancement)的引用:
  • 短字符串 (ECX <= 12):REP MOVSW/MOVSD/MOVSQ 的延迟约为20个时钟周期。
  • 快速字符串 (ECX >= 76: 不包括 REP MOVSB):处理器实现通过尽可能移动尽可能多的数据块(每次16字节)提供硬件优化。如果16字节数据传输跨越缓存行边界,则 REP 字符串的延迟将会有所不同:
  • = 无分裂:启动成本约为40个时钟周期,每64字节的数据增加4个时钟周期。
  • = 缓存分裂:启动成本约为35个时钟周期,每64字节的数据增加6个时钟周期。
  • 中间字符串长度:REP MOVSW/MOVSD/MOVSQ的延迟具有启动成本,约为15个时钟周期,每次单词/双字/四字节数据移动迭代增加1个时钟周期。

因此,根据英特尔的说法,对于非常大的内存块,REP MOVSW与REP MOVSD/MOVSQ一样快。但是,我的测试表明,在Nehalem和Westmere上,只有REP MOVSD/MOVSQ很快,而REP MOVSW甚至比REP MOVSB还要慢。

根据英特尔手册中提供的信息,在2008年之前的英特尔微架构上,启动成本甚至更高。

结论:如果您只需要复制适合L1缓存的数据,则每次复制64字节数据仅需要4个时钟周期,不需要使用XMM寄存器!

#如果数据符合L1缓存,则REP MOVSD/MOVSQ是在所有英特尔处理器上都表现优秀的通用解决方案(不需要ERMSB)#

以下是对于源和目标都在L1缓存中,且块足够大而不会受到启动成本的严重影响,但不会超过L1缓存大小的 REP MOVS*测试。 资料来源:http://users.atw.hu/instlatx64/

Yonah (2006-2008)

    REP MOVSB 10.91 B/c
    REP MOVSW 10.85 B/c
    REP MOVSD 11.05 B/c

Nehalem(2009-2010)

    REP MOVSB 25.32 B/c
    REP MOVSW 19.72 B/c
    REP MOVSD 27.56 B/c
    REP MOVSQ 27.54 B/c

Westmere(2010-2011)

    REP MOVSB 21.14 B/c
    REP MOVSW 19.11 B/c
    REP MOVSD 24.27 B/c

Ivy Bridge (2012-2013)- 带有增强型 REP MOVSB(所有后续CPU也都带有增强型 REP MOVSB)

    REP MOVSB 28.72 B/c
    REP MOVSW 19.40 B/c
    REP MOVSD 27.96 B/c
    REP MOVSQ 27.89 B/c

SkyLake(2015-2016)

    REP MOVSB 57.59 B/c
    REP MOVSW 58.20 B/c
    REP MOVSD 58.10 B/c
    REP MOVSQ 57.59 B/c

卡比湖(2016-2017)

    REP MOVSB 58.00 B/c
    REP MOVSW 57.69 B/c
    REP MOVSD 58.00 B/c
    REP MOVSQ 57.89 B/c

为了确认,我已经提供了SkyLake和Kaby Lake的测试结果 - 这些架构具有相同的CPI数据。

移动版Cannon Lake (2018年5月-2020年2月)

    REP MOVSB 107.44 B/c
    REP MOVSW 106.74 B/c
    REP MOVSD 107.08 B/c
    REP MOVSQ 107.08 B/c
Cascade Lake,服务器(2019年4月)
    REP MOVSB 58.72 B/c
    REP MOVSW 58.51 B/c
    REP MOVSD 58.51 B/c
    REP MOVSQ 58.20 B/c
    

Comet Lake,台式机,工作站,移动设备(2019年8月)

    REP MOVSB 58.72 B/c
    REP MOVSW 58.62 B/c
    REP MOVSD 58.72 B/c
    REP MOVSQ 58.72 B/c

冰湖(Ice Lake),移动设备版(2019年9月)

    REP MOVSB 102.40 B/c
    REP MOVSW 101.14 B/c
    REP MOVSD 101.14 B/c
    REP MOVSQ 101.14 B/c

特蒙特,低功耗(2020年9月)

    REP MOVSB 119.84 B/c
    REP MOVSW 121.78 B/c
    REP MOVSD 121.78 B/c
    REP MOVSQ 121.78 B/c

虎湖移动版(2020年10月)

    REP MOVSB 93.27 B/c
    REP MOVSW 93.09 B/c
    REP MOVSD 93.09 B/c
    REP MOVSQ 93.09 B/c

如您所见,REP MOVS的实现在不同的微架构中存在显著差异。 在一些处理器上,如Ivy Bridge,REP MOVSB最快,虽然仅比REP MOVSD / MOVSQ略快,但毫无疑问,在自 Nehalem以来的所有处理器上,REP MOVSD / MOVSQ都运行得非常好-甚至不需要“增强的REP MOVSB”,因为在Ivy Bridge(2013)上,使用“增强的REP MOVSB”,REP MOVSD显示与没有使用“增强的REP MOVSB”的Nehalem(2010)相同的每时钟节拍数据,而事实上,REP MOVSB只有自SkyLake(2015)以来变得非常快-比Ivy Bridge快两倍。 因此,在CPUID中的这个“增强的REP MOVSB”位可能会让人感到困惑-它只表示REP MOVSB本身OK,但并不表示任何REP MOVS*更快。

Ivy Bridge微架构上ERMSB实现最令人困惑。 是的,在很旧的处理器上,在ERMSB之前,对于大块,REP MOVS *确实使用了一个缓存协议特性,该特性对于常规代码(无RFO)不可用。 但是,这个协议在具有ERMSB的Ivy Bridge上不再使用。根据Andy Glew对Peter Cordes答案的“为什么复杂的memcpy / memset优越?”的评论,曾经在旧处理器上使用不可用于常规代码的缓存协议功能,但在Ivy Bridge上不再使用。这就解释了为什么REP MOVS *的启动成本如此之高:“选择和设置正确方法的大量开销主要是由于缺乏微码分支预测。”还有一个有趣的注释:1996年的Pentium Pro(P6)使用64位微码加载和存储以及无RFO高速缓存协议来实现REP MOVS *-不像Ivy Bridge中的ERMSB那样违反内存排序。

关于rep movsbrep movsq,在一些具有ERMSB的处理器上,rep movsb略快(例如,Xeon E3-1246 v3),在另一些处理器上,rep movsq更快(Skylake),在其他处理器上速度相同(例如i7-1065G7)。 但是,我会选择rep movsq而不是rep movsb

请注意,此答案仅适用于源数据和目标数据适合L1高速缓存的情况。 根据情况,应考虑内存访问的特殊性(高速缓存等)。 还请注意,此答案中的信息仅涉及Intel处理器,而不涉及其他制造商(如AMD)的处理器,它们可能具有更好或更差的REP MOVS *指令实现。

Tinymembench结果

以下是一些 tinymembench 的结果,以显示相对于 rep movsbrep movsd,它们的性能差异。

Intel Xeon E5-1650V3

Haswell 微架构、ERMS、AVX-2,发布于2014年9月,价格为583美元,基频为3.5 GHz,最大睿频频率:3.8 GHz(单核心),L2缓存6×256 KB,L3缓存15 MB,支持最多4 ×DDR4-2133内存,已安装了8个32768 MB DDR4 ECC reg内存模块(总计256GB内存)。

 C copy backwards                                     :   7268.8 MB/s (1.5%)
 C copy backwards (32 byte blocks)                    :   7264.3 MB/s
 C copy backwards (64 byte blocks)                    :   7271.2 MB/s
 C copy                                               :   7147.2 MB/s
 C copy prefetched (32 bytes step)                    :   7044.6 MB/s
 C copy prefetched (64 bytes step)                    :   7032.5 MB/s
 C 2-pass copy                                        :   6055.3 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6350.6 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6336.4 MB/s
 C fill                                               :  11072.2 MB/s
 C fill (shuffle within 16 byte blocks)               :  11071.3 MB/s
 C fill (shuffle within 32 byte blocks)               :  11070.8 MB/s
 C fill (shuffle within 64 byte blocks)               :  11072.0 MB/s
 ---
 standard memcpy                                      :  11608.9 MB/s
 standard memset                                      :  15789.7 MB/s
 ---
 MOVSB copy                                           :   8123.9 MB/s
 MOVSD copy                                           :   8100.9 MB/s (0.3%)
 SSE2 copy                                            :   7213.2 MB/s
 SSE2 nontemporal copy                                :  11985.5 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   7055.8 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   7044.3 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  11794.4 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  11813.1 MB/s
 SSE2 2-pass copy                                     :   6394.3 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   6255.9 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   6234.0 MB/s
 SSE2 2-pass nontemporal copy                         :   4279.5 MB/s
 SSE2 fill                                            :  10745.0 MB/s
 SSE2 nontemporal fill                                :  22014.4 MB/s

英特尔至强E3-1246 v3

哈斯威尔架构, ERMS技术, AVX-2指令集,主频3.50GHz。

 C copy backwards                                     :   6911.8 MB/s
 C copy backwards (32 byte blocks)                    :   6919.0 MB/s
 C copy backwards (64 byte blocks)                    :   6924.6 MB/s
 C copy                                               :   6934.3 MB/s (0.2%)
 C copy prefetched (32 bytes step)                    :   6860.1 MB/s
 C copy prefetched (64 bytes step)                    :   6875.6 MB/s (0.1%)
 C 2-pass copy                                        :   6471.2 MB/s
 C 2-pass copy prefetched (32 bytes step)             :   6710.3 MB/s
 C 2-pass copy prefetched (64 bytes step)             :   6745.5 MB/s (0.3%)
 C fill                                               :  10812.1 MB/s (0.2%)
 C fill (shuffle within 16 byte blocks)               :  10807.7 MB/s
 C fill (shuffle within 32 byte blocks)               :  10806.6 MB/s
 C fill (shuffle within 64 byte blocks)               :  10809.7 MB/s
 ---
 standard memcpy                                      :  10922.0 MB/s
 standard memset                                      :  28935.1 MB/s
 ---
 MOVSB copy                                           :   9656.7 MB/s
 MOVSD copy                                           :   9430.1 MB/s
 SSE2 copy                                            :   6939.1 MB/s
 SSE2 nontemporal copy                                :  10820.6 MB/s
 SSE2 copy prefetched (32 bytes step)                 :   6857.4 MB/s
 SSE2 copy prefetched (64 bytes step)                 :   6854.9 MB/s
 SSE2 nontemporal copy prefetched (32 bytes step)     :  10774.2 MB/s
 SSE2 nontemporal copy prefetched (64 bytes step)     :  10782.1 MB/s
 SSE2 2-pass copy                                     :   6683.0 MB/s
 SSE2 2-pass copy prefetched (32 bytes step)          :   6687.6 MB/s
 SSE2 2-pass copy prefetched (64 bytes step)          :   6685.8 MB/s
 SSE2 2-pass nontemporal copy                         :   5234.9 MB/s
 SSE2 fill                                            :  10622.2 MB/s
 SSE2 nontemporal fill                                :  22515.2 MB/s (0.1%)

英特尔至强 Skylake-SP

Skylake, ERMS, AVX-512, 2.1 GHz(基频Xeon Gold 6152,无Turbo)

 MOVSB copy                                           :   4619.3 MB/s (0.6%)
 SSE2 fill                                            :   9774.4 MB/s (1.5%)
 SSE2 nontemporal fill                                :   6715.7 MB/s (1.1%)

英特尔至强E3-1275V6

卡比湖,于2017年3月发布,售价为339美元,基础频率3.8 GHz,最大增强频率4.2 GHz,L2缓存4×256 KB,L3缓存8 MB,4个核心(8个线程),安装了4个16384 MB DDR4 ECC的RAM模块,但只能使用2个内存通道。

 MOVSB copy                                           :  11720.8 MB/s
 SSE2 fill                                            :  15877.6 MB/s (2.7%)
 SSE2 nontemporal fill                                :  36407.1 MB/s

英特尔i7-1065G7

冰湖, AVX-512, ERMS, FSRM, 1.37 GHz(以基础频率运行,关闭Turbo Boost模式)

MOVSB copy                                           :   7322.7 MB/s
SSE2 fill                                            :   9681.7 MB/s
SSE2 nontemporal fill                                :  16426.2 MB/s

AMD EPYC 7401P

这款处理器于2017年6月发布,售价为1075美元,基于Zen gen.1微架构,拥有24个核心(48个线程),基础频率为2.0GHz,最大睿频可达3.0GHz(少数核心)或2.8GHz(所有核心)。缓存方面,L1为每个核心64KB指令和32KB数据,L2为每个核心512KB,L3共64MB,每个CCX 8MB。内存支持DDR4-2666 8通道,但仅安装了4个32768 MB的DDR4 ECC reg.内存模块。

 MOVSB copy                                           :   7718.0 MB/s
 SSE2 fill                                            :  11233.5 MB/s
 SSE2 nontemporal fill                                :  34893.3 MB/s

AMD Ryzen 7 1700X(安装了4个RAM模块)

 MOVSB copy                                           :   7444.7 MB/s
 SSE2 fill                                            :  11100.1 MB/s
 SSE2 nontemporal fill                                :  31019.8 MB/s

AMD Ryzen 7 Pro 1700X(已安装2个RAM模块)

 MOVSB copy                                           :   7251.6 MB/s
 SSE2 fill                                            :  10691.6 MB/s
 SSE2 nontemporal fill                                :  31014.7 MB/s

AMD Ryzen 7 Pro 1700X(安装了4个RAM模块)

 MOVSB copy                                           :   7429.1 MB/s
 SSE2 fill                                            :  10954.6 MB/s
 SSE2 nontemporal fill                                :  30957.5 MB/s

结论

对于至少4KB的大内存块,如果目标内存是按照至少64字节对齐的,则REP MOVSD/MOVSQ是适用于所有英特尔处理器的通用解决方案,并且效果相对良好(无需ERMSB)。 REP MOVSD/MOVSQ在较新的处理器上(从Skylake开始)甚至表现更好。 对于Ice Lake或更新的微架构,即使对至少64字节的非常小的字符串也能完美地工作。


2
有趣的L1D中等大小缓冲区数据。然而,这可能并非全部。 ERMSB的一些好处(如存储弱排序)只会在不适合缓存的更大缓冲区中显示出来。即使在先前的ERMSB CPU上,即使用常规的快速字符串“rep movs”,也应该使用无RFO协议。 - Peter Cordes
3
如果我理解正确的话,您只是从instlatx64结果中提取了L1D专用的数字。因此结论就是,所有近期的英特尔平台上的'movsb'、'movsd'、'movsq'执行的速度大致相同。最有趣的结论可能是“不要使用‘movsw’”。您没有将其与显式循环中的'mov'指令(包括64位平台上的16字节移动,这些操作保证可用)进行比较,而在许多情况下,显式循环可能会更快。您也没有展示在AMD平台上或当大小超过L1大小时会发生什么。 - BeeOnRope
2
最后,你应该注意到除了“rep movsb”之外,没有其他东西实现了“memcpy”(它们中没有一个实现了“memmove”),因此你需要额外的代码来处理其他变体。这只在小尺寸时才有意义。 - BeeOnRope
1
是的,那句话正是我所指的。 - Peter Cordes
1
@MaximMasiutin - 你从哪里得到ERMSB不再使用不可用于常规代码的no-RFO协议?它肯定仍在使用非RFO协议,至少对于大型复制是这样,因为它获得了只有非RFO才能实现的性能(这最明显的是stosb,但也适用于mov变体)。这是否仍然“不可用于常规代码”存在争议,因为您可以通过NT存储获得类似的效果,因此不清楚“不可用于常规代码”是否仅意味着在没有NT存储的平台上,或者其他不同于NT存储的东西。 - BeeOnRope
显示剩余8条评论

9
您希望得到的是:
一个解释何时使用ERMSB有用的答案。
但我不确定它的含义。查看您提供的3.7.6.1文档,它明确表示:
使用ERMSB实现memcpy可能无法达到使用256位或128位AVX替代方案的吞吐量水平,这取决于长度和对齐因素。
因此,仅仅因为CPUID指示支持ERMSB,并不能保证REP MOVSB是复制内存最快的方法。它只是意味着它不会像以前的一些CPU那样糟糕。
但是,即使可能存在在某些条件下运行更快的替代方案,也并不意味着REP MOVSB是无用的。现在,该指令不再造成性能惩罚,因此可能再次成为有用的指令。
请记住,与我看过的某些更复杂的memcpy例程相比,这是很小的代码(2字节!)。由于加载和运行大块代码也会带来惩罚(将一些其他代码从CPU的高速缓存中清除),因此有时AVX等的“好处”将被其对其余代码的影响抵消。这取决于你做什么。
您还问道:
为什么使用REP MOVSB时带宽要低得多?我该怎么做才能改进它?
不可能“做些什么”使REP MOVSB运行更快。它只会执行它的功能。
如果您想获得从memcpy中看到的更高速度,请找到其源代码。它在某个地方出现了。或者你可以从调试器中跟踪它,并查看实际使用的代码路径。我的期望是它正在使用一些AVX指令以每次处理128位或256位。
或者,您可以...好吧,您要求我们不这样说。

我测试了在L3缓存中使用REP MOVSB的大小,它确实与SSE/AVX解决方案相竞争。但我还没有发现它明显更好。对于大于L3缓存的大小,非临时存储仍然占据优势。您关于代码大小的观点很有趣,值得考虑。我不太了解微码。REP MOVSB是通过微码实现的,因此即使它不会占用太多代码缓存并且只计为一条指令,它仍可能使用许多端口和/或微操作。 - Z boson
请看我问题的结尾,我引用了一条评论,声称ERMSB即使对于大容量存储来说也应该比非临时存储更好。 - Z boson
1
等等!你有证据表明rep movsb比其他方法更好吗?我想听更多关于这方面的内容。为了澄清,我不是在寻找只能说明rep movsb在处理大数组时更好的答案(也许这并不正确)。我会很感兴趣看到任何一个例子,证明rep movsb比其他方法更好。 - Z boson
1
这个答案真正地说明了需要说的事情。关键在于memcpy高度优化,做了各种疯狂的事情来获得最快的速度。如果你研究你的库的实现,你可能会感到惊讶。(除非你使用的是微软的编译器,那么你可能会失望,但你不会问这个问题。)很不可能你能够在速度上击败手工调整的memcpy函数,如果你能够这样做,那么Glibc的人们在为Ivy Bridge或支持这些增强功能的架构进行调整时也很可能会转向它。 - Cody Gray
REP MOVSB 的最大优势一直都是它的大小。在 8088 上这些 CISC 字符串指令非常重要,而且由于(A)总线宽度有限和(B)极小的预取队列,大小比其他大多数问题都更为重要。随着这些问题的减少,其他指令变得更快,字符串指令的性能优势也降低了。英特尔已经“增强”了它们几次(大约在 PPro 和现在的 IVB),但这只是使它们具备竞争力,它们并不是“最快的方法”。英特尔手册甚至指出了这一点。 - Cody Gray
显示剩余6条评论

9

这并不是对所述问题的答案,仅是我在尝试查找时得出的结果(和个人结论)。

总之:GCC已经优化了memset()/memmove()/memcpy()(请参见GCC源代码中的gcc/config/i386/i386.c:expand_set_or_movmem_via_rep();还可以在同一文件中查找stringop_algs以查看与架构相关的变体)。因此,在使用GCC时,没有理由指望通过使用自己的变体获得巨大的收益(除非您忘记了对齐数据的对齐属性或未启用足够特定的优化,如-O2 -march= -mtune=)。如果您同意,那么所述问题的答案在实践中或多或少是无关紧要的。

(我只希望有一个memrepeat(),它是memcpy()的相反,与memmove()相比,它会重复缓冲区的初始部分以填充整个缓冲区。)


我目前使用的是Ivy Bridge机器(Core i5-6200U笔记本电脑,Linux 4.4.0 x86-64内核,在/proc/cpuinfo标志中有erms)。因为我想找出一个情况,即基于rep movsb的自定义memcpy()变体是否能够优于直接的memcpy(),所以我编写了一个过于复杂的基准测试。

核心思想是主程序分配三个大内存区域:originalcurrentcorrect,每个区域的大小完全相同,并且至少按页对齐。复制操作被分组成集合,每个集合具有不同的属性,例如所有源和目标都对齐(到某个字节数),或者所有长度都在同一范围内。每个集合都使用srcdstn三元组的数组来描述,其中所有srcsrc+n-1dstdst+n-1都完全在current区域内。

使用 Xorshift* PRNG 用于将 original 初始化为随机数据。(如我上面所警告的那样,这过于复杂,但我想确保没有留下任何易于编译器优化的捷径。)通过在 current 中应用当前集合中的所有三元组,使用 C 库提供的 memcpy(),并将 current 区域复制到 correct,可以获得 correct 区域。这允许验证每个基准函数的正确行为。

使用相同的函数对每组复制操作进行大量计时,并使用其中位数进行比较。(在我看来,在基准测试中使用中位数是最有意义的,并提供合理的语义--该函数至少有一半的时间快于此速度。)

为避免编译器优化,我让程序在运行时动态加载函数和基准测试。这些函数都具有相同的形式:void function(void *, const void *, size_t) -- 请注意,与memcpy()memmove()不同,它们没有返回值。基准测试(即复制操作的命名集)是通过一个函数调用动态生成的(该函数接受指向current区域及其大小等参数的指针)。

不幸的是,我还没有找到任何一组数据。

static void rep_movsb(void *dst, const void *src, size_t n)
{
    __asm__ __volatile__ ( "rep movsb\n\t"
                         : "+D" (dst), "+S" (src), "+c" (n)
                         :
                         : "memory" );
}

会打败

static void normal_memcpy(void *dst, const void *src, size_t n)
{
    memcpy(dst, src, n);
}

使用GCC 5.4.0在上述Core i5-6200U笔记本电脑上运行linux-4.4.0 64位内核,使用gcc -Wall -O2 -march=ivybridge -mtune=ivybridge。复制4096字节对齐和大小的块是最接近的方法。这意味着到目前为止,我还没有发现使用rep movsb memcpy变体有意义的情况。这并不意味着没有这样的情况;只是我还没有找到。在这一点上,代码已经成为一个我感到羞耻而不是自豪的混乱,因此,除非有人要求,否则我将省略发布源代码。以上描述应该足以编写更好的代码。
这并不让我感到惊讶。C编译器可以推断操作数指针的对齐方式,以及要复制的字节数是否是编译时常量或适当2的幂的倍数。编译器可以使用这些信息来替换C库中的memcpy()/memmove()函数,且应该使用这些信息。GCC确实如此做了(请参见GCC源代码中的gcc/config/i386/i386.c:expand_set_or_movmem_via_rep(),以及在同一文件中查找stringop_algs以查看与体系结构相关的变量)。事实上,memcpy()/memset()/memmove()已经分别针对许多x86处理器变体进行了优化;如果GCC开发人员尚未包含erms支持,那将令我非常惊讶。
GCC提供了几个函数属性,开发人员可以使用这些属性来确保生成的代码质量。例如,alloc_align (n)告诉GCC该函数返回至少对齐到n字节的内存。应用程序或库可以选择在运行时使用哪个函数实现,通过创建一个“解析器函数”(返回函数指针),并使用ifunc(resolver)属性定义该函数。
我在我的代码中最常用的模式之一是:
some_type *pointer = __builtin_assume_aligned(ptr, alignment);

其中ptr是指针,alignment是它对齐的字节数;GCC知道/假定pointer对齐到alignment字节。

另一个有用的内置函数是__builtin_prefetch(),虽然使用起来要困难得多才能正确使用。为了最大化总带宽/效率,我发现在每个子操作中最小化延迟可以产生最好的结果。(对于将散布的元素复制到连续的临时存储器中,这很困难,因为预取通常涉及整个高速缓存行;如果预取太多元素,大部分高速缓存将被浪费在存储未使用的项上。)


i5-6200U笔记本电脑不是Ivy Bridge,而是Skylake。在Ivy Bridge系统上看到tingybenchmark的结果会很有趣。 - Z boson
@Zboson:非常正确;感谢您指出。我不知道我从哪里得出这个假设的,可能是凭空想象的。痛苦。这也解释了我的结果。 - Nominal Animal

4

有更有效的数据传输方式。现在,实现memcpy的编译器将生成特定于架构的代码,根据数据的内存对齐和其他因素进行优化。此举允许更好地使用非临时缓存指令以及XMM和x86世界中的其他寄存器。

当你硬编码rep movsb时,会阻止使用内置方法。

因此,就像memcpy这样的内容,除非你正在编写与特定硬件绑定的内容,或者除非你要花时间编写高度优化的汇编memcpy函数(或使用C级别的内置方法),否则最好让编译器为您解决问题。


2
实际上,使用“增强型rep movsb”时,使用“rep movsd”会变慢。在撰写答案之前,请仔细阅读此功能的含义。 - fuz
2
我在这里讨论了一个自定义的memcpy链接。其中一条评论是“请注意,在Ivybridge和Haswell上,如果缓冲区太大而无法适应MLC,则可以使用rep movsb; movntdqa击败movntdqa;rep movsb不会导致LLC中的RFO。” 我可以通过movntdqa获得与memcpy一样好的效果。我的问题是如何使用rep movsb做到至少与movntdqa一样好? - Z boson
3
这主要是为了教育目的。我试图学习有关ERMSB的知识。最终目标是从主存储器中获得尽可能高的带宽。我在我的问题中提供了我使用的代码。这就是我所做的全部内容。 - Z boson
4
这个答案似乎与“快速字符串移动”指令(如ERMSB)的现实脱节,并且重复了一个谬论,即为了获得最高性能的代码,你应该让编译器为你解决问题。当然,对于大多数代码和大多数开发者来说,要获得高性能代码,确实应该让编译器为您解决问题,但几乎总会有一定的层次,在这个层次上,精通细节的人可以使代码更快(例如因为他们更了解数据的形状等)。这个问题属于这个范畴,因为它明确提到了快速字符串操作等内容。 - BeeOnRope
4
@fuz: 实际上,在所有支持ERMSB的当前CPU上,rep movsd也显然很快。(尽管你是对的,因为Intel只记录ERMSB适用于rep movsdb/stosb - Peter Cordes
显示剩余3条评论

2
作为一般的memcpy()指南:
a) 如果要复制的数据很小(可能少于20个字节)并且具有固定大小,请让编译器完成。原因是:编译器可以使用普通的mov指令,并避免启动开销。
b) 如果要复制的数据很小(约小于4 KiB)并且保证对齐,请使用rep movsb(如果支持ERMSB)或rep movsd(如果不支持ERMSB)。原因是:在复制任何内容之前,使用SSE或AVX替代方案会有大量的“启动开销”。
c) 如果要复制的数据很小(约小于4 KiB)并且不能保证对齐,请使用rep movsb。原因是:使用SSE或AVX,或者在开始或结束时使用rep movsd进行大部分复制以及一些rep movsb,开销太大。
d) 对于所有其他情况,请使用以下内容:
    mov edx,0
.again:
    pushad
.nextByte:
    pushad
    popad
    mov al,[esi]
    pushad
    popad
    mov [edi],al
    pushad
    popad
    inc esi
    pushad
    popad
    inc edi
    pushad
    popad
    loop .nextByte
    popad
    inc edx
    cmp edx,1000
    jb .again

原因: 这种方式速度非常慢,会迫使程序员寻找另一种不涉及复制大量数据的替代方案;结果产生的软件将显著更快,因为避免了复制大量数据。


2
使用SSE或AVX替代方案在复制任何内容之前需要大量的“启动开销”。您所指的这种巨大的启动开销是什么?您能否提供更多详细信息? - Z boson
2
@Zboson:检查起始地址是否适当对齐(源和目的地都要),检查大小是否是一个好的倍数,检查是否应该使用rep movsb等(所有这些都可能导致分支预测错误)。对于大多数CPU,当你不使用它时,SSE/AVX会被关闭以节省电力,因此你可能会受到“SSE/AVX打开延迟”的影响。然后是函数调用开销(太臃肿而无法内联),其中可能包括保存/恢复调用者正在使用的任何SSE/AVX寄存器。最后,如果没有其他东西使用SSE/AVX,则在任务切换期间需要额外保存/恢复SSE/AVX状态。 - Brendan
2
@Zboson:另外,如果人们聪明点的话,他们会有多个版本,比如memcpy_small()memcpy_large_unaligned()memcpy_large_aligned()等等。这可以帮助消除一些启动开销(检查等)。不幸的是,人们比聪明更懒惰,而且(据我所知)实际上没有人做到这一点。 - Brendan
2
@BeeOnRope:这两条评论都是针对@Brendan提出的,因为我不同意他的回答。很抱歉造成了混淆,我只是想让你看到一个关于启动开销的例子,这个例子与你早先评论中提到的向量memcpy的低开销并不矛盾。 - Peter Cordes
2
在实际应用中,对于rep movsbrep movsd(以及rep movsq),对齐考虑大致相同(最近的硬件上)。当然,rep movsb在概念上是按字节移动的,但在底层,所有的字符串移动指令都试图移动更大块的字节,因此它们都受益于更好的对齐(而这种有利的对齐通常是16、32或64字节,并不与操作的原始大小真正相关)。这与一般情况下memcpy的实现如何受益于对齐类似,尽管从概念上来说它们是按字节工作的。 - BeeOnRope
显示剩余11条评论

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