为 memcpy 增强的 REP MOVSB
Enhanced REP MOVSB for memcpy
我想使用增强型 REP MOVSB (ERMSB) 为自定义 memcpy
获得高带宽。
ERMSB 是随 Ivy Bridge 微体系结构引入的。如果您不知道 ERMSB 是什么,请参阅 Intel optimization manual 中的 "Enhanced REP MOVSB and STOSB operation (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
获得 15 GB/s 和 memcpy
获得 26 GB/s 我的 i7-6700HQ (Skylake) 系统,Ubuntu 16.10,DDR4@2400 MHz 双通道 32 GB , 海湾合作委员会 6.2.
为什么 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
感兴趣的原因是基于这些评论
Note that on Ivybridge and Haswell, with buffers to large to fit in MLC you can beat movntdqa using rep movsb; movntdqa incurs a RFO into LLC, rep movsb does not...
rep movsb is significantly faster than movntdqa when streaming to memory on Ivybridge and Haswell (but be aware that pre-Ivybridge it is slow!)
What's missing/sub-optimal in this memcpy implementation?
这是我在 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 prefetched
也比 MOVSB copy
快。
在我最初的测试中,我没有禁用 Turbo。我禁用了 Turbo 并再次测试,但似乎没有太大区别。然而,改变电源管理确实有很大的不同。
当我做
sudo cpufreq-set -r -g performance
我有时会看到 20 多个 GB/s 和 rep movsb
。
和
sudo cpufreq-set -r -g powersave
我看到的最好的是大约 17 GB/s。但是memcpy
好像对电源管理不敏感。
我检查了频率(使用 turbostat
)with and without SpeedStep enabled,performance
和 powersave
用于空闲、1 核负载和 4 核负载。我运行 Intel 的MKL 密集矩阵乘法创建负载并使用OMP_SET_NUM_THREADS
设置线程数。这是结果的 table(以 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
这表明 powersave
即使禁用了 SpeedStep CPU
时钟仍然下降到 0.8 GHz
的空闲频率。只有在 performance
没有 SpeedStep 的情况下 CPU 才能以恒定频率运行。
我使用了例如 sudo cpufreq-set -r performance
(因为 cpufreq-set
给出了 st运行ge 结果)来更改电源设置。这会重新打开 Turbo,所以之后我不得不禁用 Turbo。
有更有效的方法来移动数据。如今,memcpy
的实现将从编译器生成特定于体系结构的代码,这些代码根据数据的内存对齐和其他因素进行了优化。这允许更好地使用非临时缓存指令和 XMM 以及 x86 世界中的其他寄存器。
当您进行硬编码时 rep movsb
会阻止这种内部函数的使用。
因此,对于 memcpy
之类的东西,除非您正在编写将绑定到非常特定的硬件的东西,除非您打算花时间编写高度优化的 memcpy
汇编中的函数(或使用 C 级内在函数),你 far 最好让编译器为你解决。
你说你想要:
an answer that shows when ERMSB is useful
但我不确定这意味着你认为的意思。查看您 link 的 3.7.6.1 文档,它明确表示:
implementing memcpy using ERMSB might not reach the same level of throughput as using 256-bit or 128-bit AVX alternatives, depending on length and alignment factors.
所以仅仅因为 CPUID
表示支持 ERMSB,这并不能保证 REP MOVSB 将是复制内存的最快方法。这只是意味着它不会像以前的某些 CPU 那样糟糕。
然而,仅仅因为可能有替代方案可以在某些条件下 运行 更快并不意味着 REP MOVSB 是无用的。既然这条指令曾经导致的性能损失已经消失,它可能又是一条有用的指令。
请记住,与我见过的一些更复杂的 memcpy 例程相比,它只是一小段代码(2 个字节!)。由于加载和 运行 大块代码也有一个惩罚(将你的一些其他代码从 cpu 的缓存中抛出),有时 AVX 等人的 'benefit' 会被它对其余代码的影响所抵消。取决于你在做什么。
你还问:
Why is the bandwidth so much lower with REP MOVSB? What can I do to improve it?
不可能 "do something" 使 REP MOVSB 运行 更快。它做它做的事。
如果您希望从 memcpy 中看到更高的速度,您可以挖掘它的源代码。它在外面的某个地方。或者您可以从调试器跟踪它并查看实际采用的代码路径。我的期望是它使用其中一些 AVX 指令一次处理 128 或 256 位。
或者你可以...好吧,你让我们不要说。
一般memcpy()
指南:
a) 如果被复制的数据很小(可能小于 20 字节)并且具有固定大小,让编译器来完成。原因:编译器可以使用正常的 mov
指令并避免启动开销。
b) 如果复制的数据很小(小于 4 KiB)并且保证对齐,使用 rep movsb
(如果支持 ERMSB)或 rep movsd
(如果支持 ERMSB)不支持)。原因:使用 SSE 或 AVX 替代方案在复制任何内容之前有大量 "startup overhead"。
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
原因:这会很慢,迫使程序员寻找不涉及复制大量数据的替代方案;由于避免了复制大量数据,因此生成的软件将明显更快。
这不是对所述问题的回答,只是我试图找出答案时的结果(和个人结论)。
总而言之: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 内核,erms
in /proc/cpuinfo
标志)。因为我想知道我是否能找到一个案例,其中基于 rep movsb
的自定义 memcpy() 变体的性能优于简单的 memcpy()
,我写了一个过于复杂的基准测试。
核心思想是主程序分配三个大内存区:original
、current
、correct
,每个大小完全一样,至少页对齐.复制操作被分组到集合中,每个集合具有不同的属性,比如所有源和目标都对齐(到一定数量的字节),或者所有长度都在同一范围内。每个集合都使用 src
、dst
、n
三元组数组来描述,其中所有 src
到 src+n-1
和 dst
到 dst+n-1
完全在 current
区域内。
一个Xorshift* PRNG用于初始化original
为随机数据。 (就像我上面警告的那样,这太复杂了,但我想确保我不会为编译器留下任何简单的快捷方式。)correct
区域是通过从 original
数据开始获得的 current
,应用当前集合中的所有三元组,使用C库提供的memcpy()
,将current
区域复制到correct
。这允许验证每个基准函数的行为是否正确。
每组复制操作都使用相同的函数进行多次计时,取中位数进行比较。 (在我看来,中位数在基准测试中最有意义,并提供合理的语义——该函数至少有一半的时间是那么快。)
为了避免编译器优化,我让程序在 运行 时间动态加载函数和基准测试。这些函数都具有相同的形式,void function(void *, const void *, size_t)
——请注意,与 memcpy()
和 memmove()
不同,它们 return 什么都没有。基准(命名的复制操作集)由函数调用动态生成(将指向 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 -Wall -O2 -march=ivybridge -mtune=ivybridge
在上述 Core i5-6200U 笔记本电脑上使用 GCC 5.4.0 运行 宁 linux-4.4.0 64 位内核。然而,复制 4096 字节对齐和大小的块很接近。
这意味着至少到目前为止,我还没有发现使用 rep movsb
memcpy 变体有意义的情况。这并不意味着没有这种情况。我只是还没找到。
(在这一点上,代码是一团乱麻,我感到羞愧多于自豪,所以除非有人问,否则我将省略发布源代码。不过,以上描述应该足以写出更好的代码。)
不过,这并不让我感到惊讶。 C 编译器可以推断出很多关于操作数指针对齐的信息,以及要复制的字节数是否是编译时常量,是合适的 2 的幂的倍数。编译器可以使用此信息 will/should 来用自己的函数替换 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 提供了几个 function attributes 开发人员可以使用它们来确保生成好的代码。例如,alloc_align (n)
告诉 GCC 函数 return 的内存至少对齐到 n
字节。应用程序或库可以选择在 运行 时间使用哪个函数实现,方法是创建 "resolver function"(即 return 函数指针),并使用 ifunc (resolver)
属性。
我在代码中为此使用的最常见模式之一是
some_type *pointer = __builtin_assume_aligned(ptr, alignment);
其中ptr
是一些指针,alignment
是它对齐的字节数; GCC 然后 knows/assumes pointer
对齐到 alignment
字节。
另一个有用的内置函数是 __builtin_prefetch()
,尽管更难正确使用。为了最大化整体 bandwidth/efficiency,我发现最小化每个子操作的延迟会产生最佳结果。 (对于将分散的元素复制到连续的临时存储,这很困难,因为预取通常涉及完整的缓存行;如果预取的元素太多,大部分缓存都会被存储未使用的项目浪费。)
这是一个非常接近我的主题和最近的调查,所以我将从几个角度来看待它:历史、一些技术说明(主要是学术性的)、我盒子上的测试结果,最后是一次尝试回答您关于何时何地 rep movsb
可能有意义的实际问题。
部分地,这是一个 分享结果的调用 - 如果你可以 运行 Tinymembench 并分享结果以及你的 CPU 和 RAM 配置会很棒。特别是如果您有 4 通道设置、Ivy Bridge 机箱、服务器机箱等。
历史和官方建议
快速字符串复制指令的性能历史有点像 stair-step 事件 - 即,性能停滞期与大升级交替出现,使它们符合甚至比竞争方法更快。例如,在 Nehalem(主要针对启动开销)和 Ivy Bridge(主要针对大型副本的总吞吐量)中,性能有了飞跃。您可以找到 decade-old 英特尔工程师 in this thread.
对实施 rep movs
指令的困难的见解
例如,在介绍 Ivy Bridge 之前的指南中,典型的 advice 是避免使用它们或非常小心地使用它们1.
当前(好吧,2016 年 6 月)指南有各种令人困惑且有些不一致的建议,例如2:
The specific variant of the implementation is chosen at execution time
based on data layout, alignment and the counter (ECX) value. For
example, MOVSB/STOSB with the REP prefix should be used with counter
value less than or equal to three for best performance.
那么对于 3 个或更少字节的副本?首先,您不需要 rep
前缀,因为据称启动延迟约为 9 个周期,您几乎可以肯定使用简单的 DWORD 或 QWORD mov
会更好bit-twiddling 屏蔽未使用的字节(或者可能用 2 个显式字节,字 mov
s 如果你知道大小正好是三个)。
他们接着说:
String MOVE/STORE instructions have multiple data granularities. For
efficient data movement, larger data granularities are preferable.
This means better efficiency can be achieved by decomposing an
arbitrary counter value into a number of double words plus single byte
moves with a count value less than or equal to 3.
这在当前带有 ERMSB 的硬件上肯定是错误的,其中 rep movsb
至少与 movd
或 movq
变体一样快,甚至更快。
总的来说,当前指南的第 (3.7.5) 部分既包含合理的建议,也包含严重过时的建议。这是英特尔手册的常见吞吐量,因为它们以增量方式针对每个架构进行更新(并且声称即使在当前手册中也涵盖了近二十年的架构),并且旧部分通常不会更新以替换或提供有条件的建议这不适用于当前架构。
然后他们继续在第 3.7.6 节中明确介绍 ERMSB。
我不会详尽地讨论剩余的建议,但我会在下面的 "why use it" 中总结好的部分。
该指南的其他重要声明是,在 Haswell 上,rep movsb
已得到增强,可以在内部使用 256 位操作。
技术注意事项
这只是 rep
指令从 实现的角度 .
的基本优点和缺点的快速总结
rep movs
的优点
当发出 rep
movs 指令时,CPU 知道 已知大小的整个块将被转移。这可以帮助它以离散指令无法实现的方式优化操作,例如:
- 在知道整个缓存行将被覆盖时避免 RFO 请求。
- 立即准确地发出预取请求。硬件预取在检测类似
memcpy
的模式方面做得很好,但它仍然需要几次读取才能启动,并且会 "over-prefetch" 许多缓存行超出复制区域的末尾。 rep movsb
确切知道区域大小并且可以准确预取。
显然,在 3 个 rep movs
内的商店之间无法保证顺序,这有助于简化一致性流量和其他方面的块移动,与必须遵守相当严格的内存顺序 4 的简单 mov
指令相比。
原则上,rep movs
指令可以利用 ISA 中未公开的各种架构技巧。例如,体系结构可能具有 ISA 公开的更宽的内部数据路径5 并且 rep movs
可以在内部使用它。
缺点
rep movsb
必须实现可能比底层软件要求更强的特定语义。特别是,memcpy
禁止重叠区域,因此可能会忽略这种可能性,但 rep movsb
允许它们并且必须产生预期的结果。在当前的实现中,主要影响启动开销,但可能不会影响 large-block 吞吐量。同样,rep movsb
必须支持 byte-granular 副本,即使您实际使用它来复制大块,这些块是 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
方法比 "non-temporal"7 以外的所有其他方法都快,并且比 "C" 快得多一次复制 8 个字节的方法。
- "non-temporal" 方法比
rep movs
方法快约 26% - 但比您报告的方法小得多(26 GB/s 对 15 GB/s = ~73%).
- 如果您不使用 non-temporal 存储,使用 C 中的 8 字节副本几乎与 128 位宽的 SSE load/stores 一样好。这是因为良好的复制循环可以产生足够的内存压力以使带宽饱和(例如,2.6 GHz * 1 store/cycle * 8 字节 = 26 GB/s 用于存储)。
- tinymembench 中没有明确的 256 位算法(可能 "standard"
memcpy
除外),但由于上述说明,这可能无关紧要。
- non-temporal 存储方法的吞吐量增加约为 1.45 倍,如果 NT 消除了 3 次传输中的 1 次(即 1 次读取),这非常接近您期望的 1.5 倍,NT 1 次写入 vs 2 次读取,1 次写入)。
rep movs
方法位于中间。
- 相当低的内存延迟和适度的 2 通道带宽的结合意味着这个特定的芯片恰好能够从 single-thread 饱和其内存带宽,这会显着改变行为。
rep movsd
似乎使用了与此芯片上的 rep movsb
相同的魔法。这很有趣,因为 ERMSB 仅明确针对 movsb
,而早期对 ERMSB 的早期架构的测试显示 movsb
的执行速度比 movsd
快得多。这主要是学术性的,因为无论如何 movsb
比 movsd
更通用。
哈斯韦尔
查看评论中 iwillnotexist 友情提供的 Haswell results,我们看到了相同的总体趋势(提取了最相关的结果):
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
方法仍然比 non-temporal memcpy
慢,但这里只慢了大约 14%(与 Skylake 测试中的约 26% 相比)。 NT 技术比其时间表亲的优势现在约为 57%,甚至比带宽减少的理论优势还要多一点。
什么时候应该使用 rep movs
?
最后问一下你的实际问题:什么时候或为什么要使用它?它借鉴了上述内容并引入了一些新思想。不幸的是,没有简单的答案:您必须权衡各种因素,包括一些您可能甚至无法确切知道的因素,例如未来的发展。
请注意 rep movsb
的替代方案可能是优化的 libc memcpy
(包括编译器内联的副本),或者它可能是 hand-rolled memcpy
版本。下面的一些好处仅适用于与这些替代方案中的一个或另一个进行比较(例如,"simplicity" 有助于对抗 hand-rolled 版本,但不适用于 built-in memcpy
),但有些两者都适用。
可用指令的限制
在某些环境中,某些指令或使用某些寄存器存在限制。例如,在 Linux 内核中,通常不允许使用 SSE/AVX 或 FP 寄存器。因此,大多数优化的 memcpy
变体都不能使用,因为它们依赖于 SSE 或 AVX 寄存器,并且在 x86 上使用基于普通 64 位 mov
的副本。对于这些平台,使用 rep movsb
允许优化 memcpy
的大部分性能,而不会破坏对 SIMD 代码的限制。
一个更一般的例子可能是代码必须针对多代硬件,并且不使用 hardware-specific 调度(例如,使用 cpuid
)。在这里,您可能被迫仅使用较旧的指令集,这排除了任何 AVX 等。rep movsb
在这里可能是一个好方法,因为它允许 "hidden" 访问更广泛的加载和存储,而无需使用新指令。如果你的目标是 pre-ERMSB 硬件,你必须看看 rep movsb
性能是否可以接受,但是......
面向未来
rep movsb
的一个好处是,理论上 它可以利用未来架构的架构改进,而无需更改源代码,而显式移动则不能。例如,当引入 256 位数据路径时,rep movsb
能够利用它们(如 Intel 所声称的那样),而无需对软件进行任何更改。使用 128 位移动的软件(在 Haswell 之前是最佳的)必须修改和重新编译。
所以它既是一个sftware 维护优势(无需更改源)和现有二进制文件的优势(无需部署新二进制文件即可利用改进)。
这有多重要取决于您的维护模型(例如,在实践中部署新二进制文件的频率)并且很难判断这些指令在未来可能有多快。至少英特尔通过承诺在未来至少 合理的 性能 (15.3.3.6) 来引导这个方向的使用:
REP MOVSB and REP STOSB will continue to perform reasonably well on
future processors.
与后续工作重叠
这个好处当然不会出现在一个简单的 memcpy
基准测试中,根据定义,它没有后续工作重叠,所以必须在real-world场景。充分利用 memcpy
.
周围的代码可能需要 re-organization
英特尔在其优化手册(第 11.16.3.4 节)中指出了这一优势,并用他们的话说:
When the count is known to be at least a thousand byte or more, using
enhanced REP MOVSB/STOSB can provide another advantage to amortize the
cost of the non-consuming code. The heuristic can be understood
using a value of Cnt = 4096 and memset() as example:
• A 256-bit SIMD implementation of memset() will need to issue/execute
retire 128 instances of 32- byte store operation with VMOVDQA, before
the non-consuming instruction sequences can make their way to
retirement.
• An instance of enhanced REP STOSB with ECX= 4096 is decoded as a
long micro-op flow provided by hardware, but retires as one
instruction. There are many store_data operation that must complete
before the result of memset() can be consumed. Because the completion
of store data operation is de-coupled from program-order retirement, a
substantial part of the non-consuming code stream can process through
the issue/execute and retirement, essentially cost-free if the
non-consuming sequence does not compete for store buffer resources.
所以英特尔说 rep movsb
之后的代码已经发布了一些 uops,但是虽然很多商店仍在运行中并且 rep movsb
作为一个整体还没有退休,来自以下指令的 uops 可以通过 out-of-order 机制取得比在复制循环之后出现的代码更大的进展。
来自显式加载和存储循环的微指令实际上必须按程序顺序分别退出。这必须发生才能在 ROB 中为后续 uops 腾出空间。
关于像 rep movsb
这样的微编码指令究竟能工作多长时间,似乎没有太多详细信息。我们不确切知道 micro-code 分支如何从微码定序器请求不同的微指令流,或者微指令如何退出。如果单个微指令不必单独退出,也许整个指令只占用ROB中的一个槽位?
当供给 OoO 机器的 front-end 在 uop 缓存中看到一条 rep movsb
指令时,它会激活微码定序器 ROM (MS-ROM) 以将微码 uops 发送到队列中用于 issue/rename 阶段。任何其他 uops 可能都不可能与 issue/execute8 混在一起,而 rep movsb
仍在发布,但后续指令可以是 fetched/decoded并在最后一个 rep movsb
uop 执行后立即发出,而某些副本尚未执行。
这仅在至少一些后续代码不依赖于 memcpy
的结果(这并不罕见)时才有用。
现在,这个好处的大小是有限的:除了慢 rep movsb
指令之外,您最多可以执行 N 条指令(实际上是 uops),此时您将停止,其中 N 是 ROB size。当前 ROB 大小约为 200(Haswell 为 192,Skylake 为 224),对于 IPC 为 1 的后续代码,最大收益为约 200 个免费工作周期。在 200 个周期中,您可以在 10 GB/s,因此对于该尺寸的副本,您可以获得接近副本成本的免费作品(以某种方式使副本免费)。
然而,随着副本大小变得越来越大,这一点的相对重要性会迅速降低(例如,如果您要复制 80 KB,则免费工作仅为复制成本的 1%)。尽管如此,modest-sized 份还是很有趣的。
复制循环也不会完全阻止后续指令的执行。英特尔没有详细说明收益的大小,也没有详细说明哪种副本或周边代码的收益最大。 (热或冷目标或源,高 ILP 或低 ILP high-latency 代码后)。
代码大小
与典型的优化 memcpy
例程相比,执行的代码大小(几个字节)是微不足道的。如果性能完全受到 i-cache(包括 uop 缓存)未命中的限制,则减少代码大小可能会有好处。
同样,我们可以根据副本的大小来限制此收益的大小。我实际上不会用数字计算出来,但直觉是,将动态代码大小减少 B 字节最多可以节省 C * B
cache-misses,对于某些常量 C。每个 调用 到 memcpy
会产生一次缓存未命中成本(或收益),但是更高吞吐量的优势随着复制的字节数而增加。因此对于大型传输,更高的吞吐量将主导缓存效果。
同样,这不会出现在普通基准测试中,整个循环无疑会适合 uop 缓存。您需要 real-world、in-place 测试来评估此效果。
特定于架构的优化
您报告说在您的硬件上,rep movsb
比平台 memcpy
慢得多。然而,即使在这里也有关于早期硬件(如 Ivy Bridge)的相反结果的报告。
这完全有道理,因为字符串移动操作似乎周期性地得到爱——但不是每一代因此它可能会更快或至少在它已更新的架构上绑定(此时它可能会基于其他优势获胜),只会在后续硬件中落后。
Quoting Andy Glew,在 P6 上实现这些后,谁应该知道一两件事:
the big weakness of doing fast strings in microcode was [...] the
microcode fell out of tune with every generation, getting slower and
slower until somebody got around to fixing it. Just like a library men
copy falls out of tune. I suppose that it is possible that one of the
missed opportunities was to use 128-bit loads and stores when they
became available, and so on.
在那种情况下,它可以看作是另一个 "platform specific" 优化,适用于您在标准库和 JIT 编译器中找到的典型 every-trick-in-the-book memcpy
例程:但仅适用于在更好的架构上使用。对于 JIT 或 AOT-compiled 的东西,这很容易,但对于静态编译的二进制文件,这确实需要特定于平台的调度,但通常已经存在(有时在 link 时间实现),或 mtune
参数可用于做出静态决策。
简单
即使在 Skylake 上,它似乎已经落后于绝对最快的 non-temporal 技术,但它仍然比大多数方法更快,并且 非常简单 。这意味着更少的验证时间、更少的神秘错误、更少的时间调整和更新怪物 memcpy
实现(或者,相反,如果您依赖标准库实现者的突发奇想,则更少依赖)。
延迟限制平台
内存吞吐量限制算法9 实际上可以在两个主要的总体机制中运行:DRAM 带宽限制或concurrency/latency 限制。
第一种模式您可能很熟悉:DRAM 子系统具有一定的理论带宽,您可以根据通道数、数据 rate/width 和频率很容易地计算出该带宽。例如,我的 2 通道 DDR4-2133 系统的最大带宽为 2.133 * 8 * 2 = 34.1 GB/s,与 reported on ARK.
相同
您不会承受超过插槽上所有内核添加的 DRAM 的速率(并且通常由于各种低效而略低)(即,这是 single-socket 系统的全局限制)。
另一个限制是由内核实际可以向内存子系统发出的并发请求数强加的。想象一下,如果一个核心一次只能处理 1 个请求,对于 64 字节的缓存行 - 当请求完成时,您可以发出另一个请求。还假设非常快的 50ns 内存延迟。然后,尽管 34.1 GB/s DRAM 带宽很大,但您实际上只能获得 64 字节/50 ns = 1.28 GB/s,或不到最大带宽的 4%。
在实践中,内核一次可以发出多个请求,但数量不是无限的。通常理解在 L1 和内存层次结构的其余部分之间每个内核只有 10 行填充缓冲区,在 L2 和 DRAM 之间可能有 16 个左右的填充缓冲区。预取竞争相同的资源,但至少有助于减少有效延迟。有关详细信息,请查看主要在英特尔论坛上的任何精彩帖子 Dr. Bandwidth has written on the topic。
不过,大多数最近的CPU受到这个因素的限制,而不是RAM带宽。通常他们每个核心达到 12 - 20 GB/s,而 RAM 带宽可能是 50+ GB/s(在 4 通道系统上)。只有一些最近的 gen 2 通道 "client" 核心,它们似乎有更好的非核心,也许更多的行缓冲区可以达到单个核心的 DRAM 限制,我们的 Skylake 芯片似乎是其中之一。
当然,英特尔设计具有 50 GB/s DRAM 带宽的系统是有原因的,而由于并发限制,每个内核只能维持 < 20 GB/s:前一个限制是 socket-wide,后者是每个核心。因此,8 核系统上的每个内核可以推送 20 个 GB/s 的请求,此时它们将再次受到 DRAM 限制。
为什么我要喋喋不休地谈论这个?因为最好的 memcpy
实现通常取决于您在哪种制度下运行。一旦您的 DRAM BW 受到限制(我们的芯片显然是这样,但大多数都不在单核上),使用 non-temporal 写入变得非常重要,因为它节省了通常会浪费 1/3 带宽的 read-for-ownership。您在上面的测试结果中看到了这一点:不 使用 NT 存储的 memcpy 实现丢失了 1/3 的带宽。
但是,如果您的并发性受限,情况会趋于平衡,有时会相反。您有空闲的 DRAM 带宽,因此 NT 存储无济于事,它们甚至可能会造成伤害,因为它们可能会增加延迟,因为行缓冲区的切换时间可能比预取将 RFO 行带入 LLC(甚至L2) 然后存储在 LLC 中完成以有效降低延迟。最后,server uncores 的 NT 存储往往比客户端存储慢得多(和高带宽),这加剧了这种影响。
所以在其他平台上,您可能会发现 NT 存储不太有用(至少当您关心 single-threaded 性能时)并且可能 rep movsb
获胜(如果它兼顾两者)世界s).
真的,这最后一项是对大多数测试的要求。我知道 NT 商店在大多数架构(包括当前服务器架构)上的 single-threaded 测试中失去了明显的优势,但我不知道 rep movsb
将如何相对执行...
参考资料
以上未包含的其他良好信息来源。
comp.arch investigation of rep movsb
与备选方案。很多关于分支预测的好笔记,以及我经常建议的小块方法的实现:首先使用重叠 and/or 最后 read/writes 而不是尝试只写入所需的字节数(例如,将 9 到 16 字节的所有副本实现为两个 8 字节的副本,这两个副本最多可能重叠 7 个字节)。
1 大概目的是将其限制在某些情况下,例如 code-size 非常重要。
2 参见第 3.7.5 节: REP 前缀和数据移动。
3 关键是要注意这仅适用于单个指令本身内的各种存储:一旦完成,存储块仍然相对于先前和后续的顺序出现商店。因此代码可以看到 rep movs
中的商店相对于彼此 乱序 而不是相对于先前或后续商店(这是您通常需要的后者保证)。如果您使用复制目标的末尾作为同步标志而不是单独的存储,这只会是一个问题。
4 请注意 non-temporal 离散商店也避免了大多数订购要求,尽管实际上 rep movs
有更多的自由,因为仍然有一些WC/NT 商店的订购限制。
5 这在 32 位时代后期很常见,当时许多芯片都有 64 位数据路径(例如,支持支持64 位 double
类型)。今天,"neutered" 芯片如 Pentium 或 Celeron 品牌已禁用 AVX,但据推测 rep movs
微码仍可使用 256b loads/stores.
6 例如,由于语言对齐规则、对齐属性或运算符、别名规则或编译时确定的其他信息。在对齐的情况下,即使无法确定确切的对齐方式,他们至少可以将对齐检查提升到循环之外,或者以其他方式消除冗余检查。
7 我假设 "standard" memcpy
正在选择 non-temporal 方法,这很可能适用于此尺寸缓冲区。
8 这不一定是显而易见的,因为 rep movsb
生成的 uop 流可能只是垄断了调度,然后它会看起来非常像显式 mov
的情况。然而,它似乎不是那样工作的——来自后续指令的微指令可以与来自微编码 rep movsb
.
的微指令混合在一起
9 即,那些可以发出大量独立内存请求并因此使可用 DRAM-to-core 带宽饱和的那些,其中 memcpy
将是海报child(并且与指针追逐等纯粹的延迟绑定负载相对)。
增强型 REP MOVSB(Ivy Bridge 及更高版本)
Ivy Bridge 微架构(2012 年和 2013 年发布的处理器)引入了Enhanced REP MOVSB (ERMSB)。我们仍然需要检查相应的位。 ERMS 旨在让我们使用 rep movsb
.
快速复制内存
2017 年发布的最新处理器的最便宜版本 - Kaby Lake Celeron 和 Pentium,没有可用于快速内存复制的 AVX,但仍有增强型 REP MOVSB。英特尔在 2018 年及以后发布的一些移动和低功耗架构(不基于 SkyLake)与前几代微架构相比,使用 REP MOVSB 每个 CPU 周期复制的字节数多两倍。
在具有快速短 REP MOV (FSRM) 的 Ice Lake 微体系结构之前增强的 REP MOVSB (ERMSB) 如果块大小至少为 256 字节,则仅比 AVX 复制或通用寄存器复制更快。对于 64 字节以下的块,速度要慢 很多 ,因为 ERMSB 中有一个高内部启动 - 大约 35 个周期。 FSRM 功能旨在 128 字节之前的块也很快。
参见 Intel 优化手册,第 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 转换惩罚或降低涡轮频率。所以 REP MOVSB 是一种比 AVX 更安全的内存复制方式。
关于REP MOVSB vs. AVX拷贝的对齐效果,英特尔手册给出了以下信息:
- if the source buffer is not aligned, the impact on ERMSB implementation versus 128-bit AVX is similar;
- if the destination buffer is not aligned, the effect on ERMSB implementation can be 25% degradation, while 128-bit AVX implementation of memory copy may degrade only 5%, relative to 16-byte aligned scenario.
我已经在 64 位的英特尔酷睿 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 副本(未展开)慢。仅从 256 字节块开始,ERMSB 实现就开始优于 MOV RAX 循环。
快速短 REP MOV (FSRM)
2019 年 9 月推出的 Ice Lake 微架构引入了 Fast Short REP MOV (FSRM)。此功能可以通过 CPUID 位进行测试。它的目的是让 128 字节或更少字节的字符串也更快,但实际上,使用 rep movsb
时 64 字节之前的字符串仍然比使用简单的 64 位寄存器复制慢。除此之外,FSRM 仅在 64 位下实现,而不是在 32 位下实现。至少在我的 i7-1065G7 CPU 上,rep movsb
仅对 64 位以下的小字符串很快,但在 32 位字符串上必须至少为 4KB 才能 rep movsb
开始超越其他方法。
Nehalem (2009-2013) 上的正常(未增强)REP MOVS
令人惊讶的是,以前的架构(Nehalem 和后来的架构,直到但不包括 Ivy Bridge),还没有增强的 REP MOVB,具有相对较快的 REP MOVSD/MOVSQ(但不是 REP MOVSB/MOVSW) 大块的实现,但不足以超过 L1 缓存。
Intel Optimization Manual (2.5.6 REP String Enhancement) 提供了以下与 Nehalem 微架构相关的信息 - 2009 年和 2010 年发布的 Intel Core i5、i7 和 Xeon 处理器,以及后来的微架构,包括 Sandy Bridge 制造高达2013.
REP MOVSB
如果 ECX < 4,则 MOVSB 的延迟为 9 个周期。否则,ECX > 9 的 REP MOVSB 有 50 个周期的启动成本。
- tiny string (ECX < 4): REP MOVSB 延迟为 9 个周期;
- small string (ECX在4到9之间): Intel手册上没有官方资料,大概是9个周期以上,50个周期以下;
- 长字符串 (ECX > 9):50 个周期的启动成本。
MOVSW/MOVSD/MOVSQ
引自英特尔优化手册(2.5.6 REP 字符串增强):
- 短字符串(ECX <= 12):REP MOVSW/MOVSD/MOVSQ的延迟约为20个周期。
- 快速字符串(ECX >= 76:不包括 REP MOVSB):处理器实现通过在 16 字节中移动尽可能多的数据来提供硬件优化。如果 16 字节数据传输之一跨越高速缓存行边界,则 REP 字符串延迟的延迟会有所不同:
- = Split-free:延迟包括大约 40 个周期的启动成本,每 64 字节数据增加 4 个周期。
- = 缓存拆分:延迟包括大约 35 个周期的启动成本,每 64 字节数据增加 6 个周期。
- 中等字符串长度:REP MOVSW/MOVSD/MOVSQ 的延迟启动成本约为 15 个周期加上 word/dword/qword 中数据移动的每次迭代一个周期。
因此,根据 Intel 的说法,对于非常大的内存块,REP MOVSW 与 REP MOVSD/MOVSQ 一样快。无论如何,我的测试表明只有 REP MOVSD/MOVSQ 很快,而 REP MOVSW 在 Nehalem 和 Westmere 上比 REP MOVSB 还要慢。
根据英特尔在手册中提供的信息,在以前的英特尔微架构(2008 年之前)上,启动成本更高。
结论:如果只需要复制适合L1缓存的数据,复制64字节数据只需4个周期即可,不需要使用XMM寄存器!
#REP MOVSD/MOVSQ 是通用解决方案,如果数据适合 L1 缓存,它在所有 Intel 处理器上运行良好(不需要 ERMSB)#
以下是 REP MOVS* 的测试,当源和目标位于 L1 缓存中时,块足够大,不会受到启动成本的严重影响,但不会大到超过 L1 缓存大小。资料来源:http://users.atw.hu/instlatx64/
约娜 (2006-2008)
REP MOVSB 10.91 B/c
REP MOVSW 10.85 B/c
REP MOVSD 11.05 B/c
尼哈勒姆 (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
韦斯特米尔 (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(所有后续 CPUs 也具有增强的 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
天湖 (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 的测试结果只是为了确认 - 这些架构具有相同的每指令周期数据。
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
冰湖,移动版(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
Tremont,低功耗(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
Tiger Lake,移动版(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 工作得很好 - 你甚至不需要“Enhanced REP MOVSB”,因为在 Ivy Bridge (2013) 上使用 Enhacnced REP MOVSB,REP MOVSD 显示与 Nehalem (2010) 相同的字节数据每个时钟数据没有 增强的 REP MOVSB,而事实上 REP MOVSB 仅在 SkyLake (2015) 之后才变得非常快——是 Ivy Bridge 上的两倍。因此,CPUID 中的 Enhacnced REP MOVSB 位可能令人困惑 - 它仅表明 REP MOVSB
本身是可以的,但不是任何 REP MOVS*
更快。
最令人困惑的 ERMSB 实现是在 Ivy Bridge 微体系结构上。是的,在非常老的处理器上,在 ERMSB 之前,用于大块的 REP MOVS* 确实使用了常规代码不可用的缓存协议功能(无 RFO)。但是这个协议在有 ERMSB 的 Ivy Bridge 上不再使用。根据 ,常规代码无法使用的缓存协议功能曾经用于较旧的处理器,但不再用于 Ivy Bridge。并且解释了为什么 REP MOVS* 的启动成本如此之高:“选择和设置正确方法的巨大开销主要是由于缺乏微代码分支预测”。还有一个有趣的注意事项,即 1996 年的 Pentium Pro (P6) 使用 64 位微码加载和存储以及无 RFO 缓存协议实现了 REP MOVS* - 它们没有违反内存顺序,这与 Ivy Bridge 中的 ERMSB 不同。
关于 rep movsb
与 rep movsq
,在某些带有 ERMSB 的处理器上 rep movsb
稍快(例如 Xeon E3-1246 v3),在其他 rep movsq
上是更快(Skylake),而在其他设备上速度相同(例如 i7-1065G7)。但是,无论如何,我会选择 rep movsq
而不是 rep movsb
。
另请注意,此答案仅适用于源数据和目标数据适合 L1 缓存的情况。根据具体情况,应考虑内存访问(缓存等)的特殊性。
另请注意,此答案中的信息仅与英特尔处理器相关,与其他制造商(如 AMD)的处理器无关,这些处理器可能具有更好或更差的 REP MOVS* 指令实现。
Tinymembench 结果
这里有一些 tinymembench 结果显示 rep movsb
和 rep movsd
.
的相对性能
英特尔至强 E5-1650V3
Haswell 微架构,ERMS,AVX-2,2014 年 9 月发布,售价 583 美元,基本频率 3.5 GHz,最大睿频:3.8 GHz(单核),二级缓存 6 × 256 KB,三级缓存 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
Haswell、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
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
Kaby Lake,2017 年 3 月发布,售价 339 美元,基础频率 3.8 GHz,最大睿频 4.2 GHz,二级缓存 4 × 256 KB,三级缓存 8 MB,4 核(8 线程),4 RAM 模块 16384 MB DDR4 ECC 已安装,但只能使用 2 个内存通道。
MOVSB copy : 11720.8 MB/s
SSE2 fill : 15877.6 MB/s (2.7%)
SSE2 nontemporal fill : 36407.1 MB/s
英特尔 i7-1065G7
Ice Lake,AVX-512,ERMS,FSRM,1.37 GHz(在基本频率下工作,禁用涡轮模式)
MOVSB copy : 7322.7 MB/s
SSE2 fill : 9681.7 MB/s
SSE2 nontemporal fill : 16426.2 MB/s
AMD 霄龙 7401P
2017 年 6 月发布,售价 1075 美元,基于 Zen gen.1 微架构,24 核(48 线程),基本频率:2.0GHz,最大睿频:3.0GHz(少数核)或 2.8(所有核) ;高速缓存:L1 - 64 KB 实例。 & 每个核心 32 KB 数据,L2 - 每个核心 512 KB,L3 - 64 MB,每个 CCX 8 MB,DDR4-2666 8 通道,但只有 4 个 32768 MB 的 RAM 模块,每个 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
结论
REP MOVSD/MOVSQ 是通用解决方案,如果目标至少对齐 64 字节,则它在所有 Intel 处理器上对于至少 4KB 的大内存块(不需要 ERMSB)工作得相对较好。
REP MOVSD/MOVSQ 在更新的处理器上工作得更好,从 Skylake 开始。而且,对于 Ice Lake 或更新的微架构,它甚至可以完美地处理至少 64 字节的非常小的字符串。
我想使用增强型 REP MOVSB (ERMSB) 为自定义 memcpy
获得高带宽。
ERMSB 是随 Ivy Bridge 微体系结构引入的。如果您不知道 ERMSB 是什么,请参阅 Intel optimization manual 中的 "Enhanced REP MOVSB and STOSB operation (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
获得 15 GB/s 和 memcpy
获得 26 GB/s 我的 i7-6700HQ (Skylake) 系统,Ubuntu 16.10,DDR4@2400 MHz 双通道 32 GB , 海湾合作委员会 6.2.
为什么 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
感兴趣的原因是基于这些评论
Note that on Ivybridge and Haswell, with buffers to large to fit in MLC you can beat movntdqa using rep movsb; movntdqa incurs a RFO into LLC, rep movsb does not... rep movsb is significantly faster than movntdqa when streaming to memory on Ivybridge and Haswell (but be aware that pre-Ivybridge it is slow!)
What's missing/sub-optimal in this memcpy implementation?
这是我在 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 prefetched
也比 MOVSB copy
快。
在我最初的测试中,我没有禁用 Turbo。我禁用了 Turbo 并再次测试,但似乎没有太大区别。然而,改变电源管理确实有很大的不同。
当我做
sudo cpufreq-set -r -g performance
我有时会看到 20 多个 GB/s 和 rep movsb
。
和
sudo cpufreq-set -r -g powersave
我看到的最好的是大约 17 GB/s。但是memcpy
好像对电源管理不敏感。
我检查了频率(使用 turbostat
)with and without SpeedStep enabled,performance
和 powersave
用于空闲、1 核负载和 4 核负载。我运行 Intel 的MKL 密集矩阵乘法创建负载并使用OMP_SET_NUM_THREADS
设置线程数。这是结果的 table(以 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
这表明 powersave
即使禁用了 SpeedStep CPU
时钟仍然下降到 0.8 GHz
的空闲频率。只有在 performance
没有 SpeedStep 的情况下 CPU 才能以恒定频率运行。
我使用了例如 sudo cpufreq-set -r performance
(因为 cpufreq-set
给出了 st运行ge 结果)来更改电源设置。这会重新打开 Turbo,所以之后我不得不禁用 Turbo。
有更有效的方法来移动数据。如今,memcpy
的实现将从编译器生成特定于体系结构的代码,这些代码根据数据的内存对齐和其他因素进行了优化。这允许更好地使用非临时缓存指令和 XMM 以及 x86 世界中的其他寄存器。
当您进行硬编码时 rep movsb
会阻止这种内部函数的使用。
因此,对于 memcpy
之类的东西,除非您正在编写将绑定到非常特定的硬件的东西,除非您打算花时间编写高度优化的 memcpy
汇编中的函数(或使用 C 级内在函数),你 far 最好让编译器为你解决。
你说你想要:
an answer that shows when ERMSB is useful
但我不确定这意味着你认为的意思。查看您 link 的 3.7.6.1 文档,它明确表示:
implementing memcpy using ERMSB might not reach the same level of throughput as using 256-bit or 128-bit AVX alternatives, depending on length and alignment factors.
所以仅仅因为 CPUID
表示支持 ERMSB,这并不能保证 REP MOVSB 将是复制内存的最快方法。这只是意味着它不会像以前的某些 CPU 那样糟糕。
然而,仅仅因为可能有替代方案可以在某些条件下 运行 更快并不意味着 REP MOVSB 是无用的。既然这条指令曾经导致的性能损失已经消失,它可能又是一条有用的指令。
请记住,与我见过的一些更复杂的 memcpy 例程相比,它只是一小段代码(2 个字节!)。由于加载和 运行 大块代码也有一个惩罚(将你的一些其他代码从 cpu 的缓存中抛出),有时 AVX 等人的 'benefit' 会被它对其余代码的影响所抵消。取决于你在做什么。
你还问:
Why is the bandwidth so much lower with REP MOVSB? What can I do to improve it?
不可能 "do something" 使 REP MOVSB 运行 更快。它做它做的事。
如果您希望从 memcpy 中看到更高的速度,您可以挖掘它的源代码。它在外面的某个地方。或者您可以从调试器跟踪它并查看实际采用的代码路径。我的期望是它使用其中一些 AVX 指令一次处理 128 或 256 位。
或者你可以...好吧,你让我们不要说。
一般memcpy()
指南:
a) 如果被复制的数据很小(可能小于 20 字节)并且具有固定大小,让编译器来完成。原因:编译器可以使用正常的 mov
指令并避免启动开销。
b) 如果复制的数据很小(小于 4 KiB)并且保证对齐,使用 rep movsb
(如果支持 ERMSB)或 rep movsd
(如果支持 ERMSB)不支持)。原因:使用 SSE 或 AVX 替代方案在复制任何内容之前有大量 "startup overhead"。
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
原因:这会很慢,迫使程序员寻找不涉及复制大量数据的替代方案;由于避免了复制大量数据,因此生成的软件将明显更快。
这不是对所述问题的回答,只是我试图找出答案时的结果(和个人结论)。
总而言之: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 内核,erms
in /proc/cpuinfo
标志)。因为我想知道我是否能找到一个案例,其中基于 rep movsb
的自定义 memcpy() 变体的性能优于简单的 memcpy()
,我写了一个过于复杂的基准测试。
核心思想是主程序分配三个大内存区:original
、current
、correct
,每个大小完全一样,至少页对齐.复制操作被分组到集合中,每个集合具有不同的属性,比如所有源和目标都对齐(到一定数量的字节),或者所有长度都在同一范围内。每个集合都使用 src
、dst
、n
三元组数组来描述,其中所有 src
到 src+n-1
和 dst
到 dst+n-1
完全在 current
区域内。
一个Xorshift* PRNG用于初始化original
为随机数据。 (就像我上面警告的那样,这太复杂了,但我想确保我不会为编译器留下任何简单的快捷方式。)correct
区域是通过从 original
数据开始获得的 current
,应用当前集合中的所有三元组,使用C库提供的memcpy()
,将current
区域复制到correct
。这允许验证每个基准函数的行为是否正确。
每组复制操作都使用相同的函数进行多次计时,取中位数进行比较。 (在我看来,中位数在基准测试中最有意义,并提供合理的语义——该函数至少有一半的时间是那么快。)
为了避免编译器优化,我让程序在 运行 时间动态加载函数和基准测试。这些函数都具有相同的形式,void function(void *, const void *, size_t)
——请注意,与 memcpy()
和 memmove()
不同,它们 return 什么都没有。基准(命名的复制操作集)由函数调用动态生成(将指向 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 -Wall -O2 -march=ivybridge -mtune=ivybridge
在上述 Core i5-6200U 笔记本电脑上使用 GCC 5.4.0 运行 宁 linux-4.4.0 64 位内核。然而,复制 4096 字节对齐和大小的块很接近。
这意味着至少到目前为止,我还没有发现使用 rep movsb
memcpy 变体有意义的情况。这并不意味着没有这种情况。我只是还没找到。
(在这一点上,代码是一团乱麻,我感到羞愧多于自豪,所以除非有人问,否则我将省略发布源代码。不过,以上描述应该足以写出更好的代码。)
不过,这并不让我感到惊讶。 C 编译器可以推断出很多关于操作数指针对齐的信息,以及要复制的字节数是否是编译时常量,是合适的 2 的幂的倍数。编译器可以使用此信息 will/should 来用自己的函数替换 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 提供了几个 function attributes 开发人员可以使用它们来确保生成好的代码。例如,alloc_align (n)
告诉 GCC 函数 return 的内存至少对齐到 n
字节。应用程序或库可以选择在 运行 时间使用哪个函数实现,方法是创建 "resolver function"(即 return 函数指针),并使用 ifunc (resolver)
属性。
我在代码中为此使用的最常见模式之一是
some_type *pointer = __builtin_assume_aligned(ptr, alignment);
其中ptr
是一些指针,alignment
是它对齐的字节数; GCC 然后 knows/assumes pointer
对齐到 alignment
字节。
另一个有用的内置函数是 __builtin_prefetch()
,尽管更难正确使用。为了最大化整体 bandwidth/efficiency,我发现最小化每个子操作的延迟会产生最佳结果。 (对于将分散的元素复制到连续的临时存储,这很困难,因为预取通常涉及完整的缓存行;如果预取的元素太多,大部分缓存都会被存储未使用的项目浪费。)
这是一个非常接近我的主题和最近的调查,所以我将从几个角度来看待它:历史、一些技术说明(主要是学术性的)、我盒子上的测试结果,最后是一次尝试回答您关于何时何地 rep movsb
可能有意义的实际问题。
部分地,这是一个 分享结果的调用 - 如果你可以 运行 Tinymembench 并分享结果以及你的 CPU 和 RAM 配置会很棒。特别是如果您有 4 通道设置、Ivy Bridge 机箱、服务器机箱等。
历史和官方建议
快速字符串复制指令的性能历史有点像 stair-step 事件 - 即,性能停滞期与大升级交替出现,使它们符合甚至比竞争方法更快。例如,在 Nehalem(主要针对启动开销)和 Ivy Bridge(主要针对大型副本的总吞吐量)中,性能有了飞跃。您可以找到 decade-old 英特尔工程师 in this thread.
对实施rep movs
指令的困难的见解
例如,在介绍 Ivy Bridge 之前的指南中,典型的 advice 是避免使用它们或非常小心地使用它们1.
当前(好吧,2016 年 6 月)指南有各种令人困惑且有些不一致的建议,例如2:
The specific variant of the implementation is chosen at execution time based on data layout, alignment and the counter (ECX) value. For example, MOVSB/STOSB with the REP prefix should be used with counter value less than or equal to three for best performance.
那么对于 3 个或更少字节的副本?首先,您不需要 rep
前缀,因为据称启动延迟约为 9 个周期,您几乎可以肯定使用简单的 DWORD 或 QWORD mov
会更好bit-twiddling 屏蔽未使用的字节(或者可能用 2 个显式字节,字 mov
s 如果你知道大小正好是三个)。
他们接着说:
String MOVE/STORE instructions have multiple data granularities. For efficient data movement, larger data granularities are preferable. This means better efficiency can be achieved by decomposing an arbitrary counter value into a number of double words plus single byte moves with a count value less than or equal to 3.
这在当前带有 ERMSB 的硬件上肯定是错误的,其中 rep movsb
至少与 movd
或 movq
变体一样快,甚至更快。
总的来说,当前指南的第 (3.7.5) 部分既包含合理的建议,也包含严重过时的建议。这是英特尔手册的常见吞吐量,因为它们以增量方式针对每个架构进行更新(并且声称即使在当前手册中也涵盖了近二十年的架构),并且旧部分通常不会更新以替换或提供有条件的建议这不适用于当前架构。
然后他们继续在第 3.7.6 节中明确介绍 ERMSB。
我不会详尽地讨论剩余的建议,但我会在下面的 "why use it" 中总结好的部分。
该指南的其他重要声明是,在 Haswell 上,rep movsb
已得到增强,可以在内部使用 256 位操作。
技术注意事项
这只是 rep
指令从 实现的角度 .
rep movs
的优点
当发出
rep
movs 指令时,CPU 知道 已知大小的整个块将被转移。这可以帮助它以离散指令无法实现的方式优化操作,例如:- 在知道整个缓存行将被覆盖时避免 RFO 请求。
- 立即准确地发出预取请求。硬件预取在检测类似
memcpy
的模式方面做得很好,但它仍然需要几次读取才能启动,并且会 "over-prefetch" 许多缓存行超出复制区域的末尾。rep movsb
确切知道区域大小并且可以准确预取。
显然,在 3 个
rep movs
内的商店之间无法保证顺序,这有助于简化一致性流量和其他方面的块移动,与必须遵守相当严格的内存顺序 4 的简单mov
指令相比。原则上,
rep movs
指令可以利用 ISA 中未公开的各种架构技巧。例如,体系结构可能具有 ISA 公开的更宽的内部数据路径5 并且rep movs
可以在内部使用它。
缺点
rep movsb
必须实现可能比底层软件要求更强的特定语义。特别是,memcpy
禁止重叠区域,因此可能会忽略这种可能性,但rep movsb
允许它们并且必须产生预期的结果。在当前的实现中,主要影响启动开销,但可能不会影响 large-block 吞吐量。同样,rep movsb
必须支持 byte-granular 副本,即使您实际使用它来复制大块,这些块是 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
方法比 "non-temporal"7 以外的所有其他方法都快,并且比 "C" 快得多一次复制 8 个字节的方法。- "non-temporal" 方法比
rep movs
方法快约 26% - 但比您报告的方法小得多(26 GB/s 对 15 GB/s = ~73%). - 如果您不使用 non-temporal 存储,使用 C 中的 8 字节副本几乎与 128 位宽的 SSE load/stores 一样好。这是因为良好的复制循环可以产生足够的内存压力以使带宽饱和(例如,2.6 GHz * 1 store/cycle * 8 字节 = 26 GB/s 用于存储)。
- tinymembench 中没有明确的 256 位算法(可能 "standard"
memcpy
除外),但由于上述说明,这可能无关紧要。 - non-temporal 存储方法的吞吐量增加约为 1.45 倍,如果 NT 消除了 3 次传输中的 1 次(即 1 次读取),这非常接近您期望的 1.5 倍,NT 1 次写入 vs 2 次读取,1 次写入)。
rep movs
方法位于中间。 - 相当低的内存延迟和适度的 2 通道带宽的结合意味着这个特定的芯片恰好能够从 single-thread 饱和其内存带宽,这会显着改变行为。
rep movsd
似乎使用了与此芯片上的rep movsb
相同的魔法。这很有趣,因为 ERMSB 仅明确针对movsb
,而早期对 ERMSB 的早期架构的测试显示movsb
的执行速度比movsd
快得多。这主要是学术性的,因为无论如何movsb
比movsd
更通用。
哈斯韦尔
查看评论中 iwillnotexist 友情提供的 Haswell results,我们看到了相同的总体趋势(提取了最相关的结果):
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
方法仍然比 non-temporal memcpy
慢,但这里只慢了大约 14%(与 Skylake 测试中的约 26% 相比)。 NT 技术比其时间表亲的优势现在约为 57%,甚至比带宽减少的理论优势还要多一点。
什么时候应该使用 rep movs
?
最后问一下你的实际问题:什么时候或为什么要使用它?它借鉴了上述内容并引入了一些新思想。不幸的是,没有简单的答案:您必须权衡各种因素,包括一些您可能甚至无法确切知道的因素,例如未来的发展。
请注意 rep movsb
的替代方案可能是优化的 libc memcpy
(包括编译器内联的副本),或者它可能是 hand-rolled memcpy
版本。下面的一些好处仅适用于与这些替代方案中的一个或另一个进行比较(例如,"simplicity" 有助于对抗 hand-rolled 版本,但不适用于 built-in memcpy
),但有些两者都适用。
可用指令的限制
在某些环境中,某些指令或使用某些寄存器存在限制。例如,在 Linux 内核中,通常不允许使用 SSE/AVX 或 FP 寄存器。因此,大多数优化的 memcpy
变体都不能使用,因为它们依赖于 SSE 或 AVX 寄存器,并且在 x86 上使用基于普通 64 位 mov
的副本。对于这些平台,使用 rep movsb
允许优化 memcpy
的大部分性能,而不会破坏对 SIMD 代码的限制。
一个更一般的例子可能是代码必须针对多代硬件,并且不使用 hardware-specific 调度(例如,使用 cpuid
)。在这里,您可能被迫仅使用较旧的指令集,这排除了任何 AVX 等。rep movsb
在这里可能是一个好方法,因为它允许 "hidden" 访问更广泛的加载和存储,而无需使用新指令。如果你的目标是 pre-ERMSB 硬件,你必须看看 rep movsb
性能是否可以接受,但是......
面向未来
rep movsb
的一个好处是,理论上 它可以利用未来架构的架构改进,而无需更改源代码,而显式移动则不能。例如,当引入 256 位数据路径时,rep movsb
能够利用它们(如 Intel 所声称的那样),而无需对软件进行任何更改。使用 128 位移动的软件(在 Haswell 之前是最佳的)必须修改和重新编译。
所以它既是一个sftware 维护优势(无需更改源)和现有二进制文件的优势(无需部署新二进制文件即可利用改进)。
这有多重要取决于您的维护模型(例如,在实践中部署新二进制文件的频率)并且很难判断这些指令在未来可能有多快。至少英特尔通过承诺在未来至少 合理的 性能 (15.3.3.6) 来引导这个方向的使用:
REP MOVSB and REP STOSB will continue to perform reasonably well on future processors.
与后续工作重叠
这个好处当然不会出现在一个简单的 memcpy
基准测试中,根据定义,它没有后续工作重叠,所以必须在real-world场景。充分利用 memcpy
.
英特尔在其优化手册(第 11.16.3.4 节)中指出了这一优势,并用他们的话说:
When the count is known to be at least a thousand byte or more, using enhanced REP MOVSB/STOSB can provide another advantage to amortize the cost of the non-consuming code. The heuristic can be understood using a value of Cnt = 4096 and memset() as example:
• A 256-bit SIMD implementation of memset() will need to issue/execute retire 128 instances of 32- byte store operation with VMOVDQA, before the non-consuming instruction sequences can make their way to retirement.
• An instance of enhanced REP STOSB with ECX= 4096 is decoded as a long micro-op flow provided by hardware, but retires as one instruction. There are many store_data operation that must complete before the result of memset() can be consumed. Because the completion of store data operation is de-coupled from program-order retirement, a substantial part of the non-consuming code stream can process through the issue/execute and retirement, essentially cost-free if the non-consuming sequence does not compete for store buffer resources.
所以英特尔说 rep movsb
之后的代码已经发布了一些 uops,但是虽然很多商店仍在运行中并且 rep movsb
作为一个整体还没有退休,来自以下指令的 uops 可以通过 out-of-order 机制取得比在复制循环之后出现的代码更大的进展。
来自显式加载和存储循环的微指令实际上必须按程序顺序分别退出。这必须发生才能在 ROB 中为后续 uops 腾出空间。
关于像 rep movsb
这样的微编码指令究竟能工作多长时间,似乎没有太多详细信息。我们不确切知道 micro-code 分支如何从微码定序器请求不同的微指令流,或者微指令如何退出。如果单个微指令不必单独退出,也许整个指令只占用ROB中的一个槽位?
当供给 OoO 机器的 front-end 在 uop 缓存中看到一条 rep movsb
指令时,它会激活微码定序器 ROM (MS-ROM) 以将微码 uops 发送到队列中用于 issue/rename 阶段。任何其他 uops 可能都不可能与 issue/execute8 混在一起,而 rep movsb
仍在发布,但后续指令可以是 fetched/decoded并在最后一个 rep movsb
uop 执行后立即发出,而某些副本尚未执行。
这仅在至少一些后续代码不依赖于 memcpy
的结果(这并不罕见)时才有用。
现在,这个好处的大小是有限的:除了慢 rep movsb
指令之外,您最多可以执行 N 条指令(实际上是 uops),此时您将停止,其中 N 是 ROB size。当前 ROB 大小约为 200(Haswell 为 192,Skylake 为 224),对于 IPC 为 1 的后续代码,最大收益为约 200 个免费工作周期。在 200 个周期中,您可以在 10 GB/s,因此对于该尺寸的副本,您可以获得接近副本成本的免费作品(以某种方式使副本免费)。
然而,随着副本大小变得越来越大,这一点的相对重要性会迅速降低(例如,如果您要复制 80 KB,则免费工作仅为复制成本的 1%)。尽管如此,modest-sized 份还是很有趣的。
复制循环也不会完全阻止后续指令的执行。英特尔没有详细说明收益的大小,也没有详细说明哪种副本或周边代码的收益最大。 (热或冷目标或源,高 ILP 或低 ILP high-latency 代码后)。
代码大小
与典型的优化 memcpy
例程相比,执行的代码大小(几个字节)是微不足道的。如果性能完全受到 i-cache(包括 uop 缓存)未命中的限制,则减少代码大小可能会有好处。
同样,我们可以根据副本的大小来限制此收益的大小。我实际上不会用数字计算出来,但直觉是,将动态代码大小减少 B 字节最多可以节省 C * B
cache-misses,对于某些常量 C。每个 调用 到 memcpy
会产生一次缓存未命中成本(或收益),但是更高吞吐量的优势随着复制的字节数而增加。因此对于大型传输,更高的吞吐量将主导缓存效果。
同样,这不会出现在普通基准测试中,整个循环无疑会适合 uop 缓存。您需要 real-world、in-place 测试来评估此效果。
特定于架构的优化
您报告说在您的硬件上,rep movsb
比平台 memcpy
慢得多。然而,即使在这里也有关于早期硬件(如 Ivy Bridge)的相反结果的报告。
这完全有道理,因为字符串移动操作似乎周期性地得到爱——但不是每一代因此它可能会更快或至少在它已更新的架构上绑定(此时它可能会基于其他优势获胜),只会在后续硬件中落后。
Quoting Andy Glew,在 P6 上实现这些后,谁应该知道一两件事:
the big weakness of doing fast strings in microcode was [...] the microcode fell out of tune with every generation, getting slower and slower until somebody got around to fixing it. Just like a library men copy falls out of tune. I suppose that it is possible that one of the missed opportunities was to use 128-bit loads and stores when they became available, and so on.
在那种情况下,它可以看作是另一个 "platform specific" 优化,适用于您在标准库和 JIT 编译器中找到的典型 every-trick-in-the-book memcpy
例程:但仅适用于在更好的架构上使用。对于 JIT 或 AOT-compiled 的东西,这很容易,但对于静态编译的二进制文件,这确实需要特定于平台的调度,但通常已经存在(有时在 link 时间实现),或 mtune
参数可用于做出静态决策。
简单
即使在 Skylake 上,它似乎已经落后于绝对最快的 non-temporal 技术,但它仍然比大多数方法更快,并且 非常简单 。这意味着更少的验证时间、更少的神秘错误、更少的时间调整和更新怪物 memcpy
实现(或者,相反,如果您依赖标准库实现者的突发奇想,则更少依赖)。
延迟限制平台
内存吞吐量限制算法9 实际上可以在两个主要的总体机制中运行:DRAM 带宽限制或concurrency/latency 限制。
第一种模式您可能很熟悉:DRAM 子系统具有一定的理论带宽,您可以根据通道数、数据 rate/width 和频率很容易地计算出该带宽。例如,我的 2 通道 DDR4-2133 系统的最大带宽为 2.133 * 8 * 2 = 34.1 GB/s,与 reported on ARK.
相同您不会承受超过插槽上所有内核添加的 DRAM 的速率(并且通常由于各种低效而略低)(即,这是 single-socket 系统的全局限制)。
另一个限制是由内核实际可以向内存子系统发出的并发请求数强加的。想象一下,如果一个核心一次只能处理 1 个请求,对于 64 字节的缓存行 - 当请求完成时,您可以发出另一个请求。还假设非常快的 50ns 内存延迟。然后,尽管 34.1 GB/s DRAM 带宽很大,但您实际上只能获得 64 字节/50 ns = 1.28 GB/s,或不到最大带宽的 4%。
在实践中,内核一次可以发出多个请求,但数量不是无限的。通常理解在 L1 和内存层次结构的其余部分之间每个内核只有 10 行填充缓冲区,在 L2 和 DRAM 之间可能有 16 个左右的填充缓冲区。预取竞争相同的资源,但至少有助于减少有效延迟。有关详细信息,请查看主要在英特尔论坛上的任何精彩帖子 Dr. Bandwidth has written on the topic。
不过,大多数最近的CPU受到这个因素的限制,而不是RAM带宽。通常他们每个核心达到 12 - 20 GB/s,而 RAM 带宽可能是 50+ GB/s(在 4 通道系统上)。只有一些最近的 gen 2 通道 "client" 核心,它们似乎有更好的非核心,也许更多的行缓冲区可以达到单个核心的 DRAM 限制,我们的 Skylake 芯片似乎是其中之一。
当然,英特尔设计具有 50 GB/s DRAM 带宽的系统是有原因的,而由于并发限制,每个内核只能维持 < 20 GB/s:前一个限制是 socket-wide,后者是每个核心。因此,8 核系统上的每个内核可以推送 20 个 GB/s 的请求,此时它们将再次受到 DRAM 限制。
为什么我要喋喋不休地谈论这个?因为最好的 memcpy
实现通常取决于您在哪种制度下运行。一旦您的 DRAM BW 受到限制(我们的芯片显然是这样,但大多数都不在单核上),使用 non-temporal 写入变得非常重要,因为它节省了通常会浪费 1/3 带宽的 read-for-ownership。您在上面的测试结果中看到了这一点:不 使用 NT 存储的 memcpy 实现丢失了 1/3 的带宽。
但是,如果您的并发性受限,情况会趋于平衡,有时会相反。您有空闲的 DRAM 带宽,因此 NT 存储无济于事,它们甚至可能会造成伤害,因为它们可能会增加延迟,因为行缓冲区的切换时间可能比预取将 RFO 行带入 LLC(甚至L2) 然后存储在 LLC 中完成以有效降低延迟。最后,server uncores 的 NT 存储往往比客户端存储慢得多(和高带宽),这加剧了这种影响。
所以在其他平台上,您可能会发现 NT 存储不太有用(至少当您关心 single-threaded 性能时)并且可能 rep movsb
获胜(如果它兼顾两者)世界s).
真的,这最后一项是对大多数测试的要求。我知道 NT 商店在大多数架构(包括当前服务器架构)上的 single-threaded 测试中失去了明显的优势,但我不知道 rep movsb
将如何相对执行...
参考资料
以上未包含的其他良好信息来源。
comp.arch investigation of rep movsb
与备选方案。很多关于分支预测的好笔记,以及我经常建议的小块方法的实现:首先使用重叠 and/or 最后 read/writes 而不是尝试只写入所需的字节数(例如,将 9 到 16 字节的所有副本实现为两个 8 字节的副本,这两个副本最多可能重叠 7 个字节)。
1 大概目的是将其限制在某些情况下,例如 code-size 非常重要。
2 参见第 3.7.5 节: REP 前缀和数据移动。
3 关键是要注意这仅适用于单个指令本身内的各种存储:一旦完成,存储块仍然相对于先前和后续的顺序出现商店。因此代码可以看到 rep movs
中的商店相对于彼此 乱序 而不是相对于先前或后续商店(这是您通常需要的后者保证)。如果您使用复制目标的末尾作为同步标志而不是单独的存储,这只会是一个问题。
4 请注意 non-temporal 离散商店也避免了大多数订购要求,尽管实际上 rep movs
有更多的自由,因为仍然有一些WC/NT 商店的订购限制。
5 这在 32 位时代后期很常见,当时许多芯片都有 64 位数据路径(例如,支持支持64 位 double
类型)。今天,"neutered" 芯片如 Pentium 或 Celeron 品牌已禁用 AVX,但据推测 rep movs
微码仍可使用 256b loads/stores.
6 例如,由于语言对齐规则、对齐属性或运算符、别名规则或编译时确定的其他信息。在对齐的情况下,即使无法确定确切的对齐方式,他们至少可以将对齐检查提升到循环之外,或者以其他方式消除冗余检查。
7 我假设 "standard" memcpy
正在选择 non-temporal 方法,这很可能适用于此尺寸缓冲区。
8 这不一定是显而易见的,因为 rep movsb
生成的 uop 流可能只是垄断了调度,然后它会看起来非常像显式 mov
的情况。然而,它似乎不是那样工作的——来自后续指令的微指令可以与来自微编码 rep movsb
.
9 即,那些可以发出大量独立内存请求并因此使可用 DRAM-to-core 带宽饱和的那些,其中 memcpy
将是海报child(并且与指针追逐等纯粹的延迟绑定负载相对)。
增强型 REP MOVSB(Ivy Bridge 及更高版本)
Ivy Bridge 微架构(2012 年和 2013 年发布的处理器)引入了Enhanced REP MOVSB (ERMSB)。我们仍然需要检查相应的位。 ERMS 旨在让我们使用 rep movsb
.
2017 年发布的最新处理器的最便宜版本 - Kaby Lake Celeron 和 Pentium,没有可用于快速内存复制的 AVX,但仍有增强型 REP MOVSB。英特尔在 2018 年及以后发布的一些移动和低功耗架构(不基于 SkyLake)与前几代微架构相比,使用 REP MOVSB 每个 CPU 周期复制的字节数多两倍。
在具有快速短 REP MOV (FSRM) 的 Ice Lake 微体系结构之前增强的 REP MOVSB (ERMSB) 如果块大小至少为 256 字节,则仅比 AVX 复制或通用寄存器复制更快。对于 64 字节以下的块,速度要慢 很多 ,因为 ERMSB 中有一个高内部启动 - 大约 35 个周期。 FSRM 功能旨在 128 字节之前的块也很快。
参见 Intel 优化手册,第 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 转换惩罚或降低涡轮频率。所以 REP MOVSB 是一种比 AVX 更安全的内存复制方式。
关于REP MOVSB vs. AVX拷贝的对齐效果,英特尔手册给出了以下信息:
- if the source buffer is not aligned, the impact on ERMSB implementation versus 128-bit AVX is similar;
- if the destination buffer is not aligned, the effect on ERMSB implementation can be 25% degradation, while 128-bit AVX implementation of memory copy may degrade only 5%, relative to 16-byte aligned scenario.
我已经在 64 位的英特尔酷睿 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 副本(未展开)慢。仅从 256 字节块开始,ERMSB 实现就开始优于 MOV RAX 循环。
快速短 REP MOV (FSRM)
2019 年 9 月推出的 Ice Lake 微架构引入了 Fast Short REP MOV (FSRM)。此功能可以通过 CPUID 位进行测试。它的目的是让 128 字节或更少字节的字符串也更快,但实际上,使用 rep movsb
时 64 字节之前的字符串仍然比使用简单的 64 位寄存器复制慢。除此之外,FSRM 仅在 64 位下实现,而不是在 32 位下实现。至少在我的 i7-1065G7 CPU 上,rep movsb
仅对 64 位以下的小字符串很快,但在 32 位字符串上必须至少为 4KB 才能 rep movsb
开始超越其他方法。
Nehalem (2009-2013) 上的正常(未增强)REP MOVS
令人惊讶的是,以前的架构(Nehalem 和后来的架构,直到但不包括 Ivy Bridge),还没有增强的 REP MOVB,具有相对较快的 REP MOVSD/MOVSQ(但不是 REP MOVSB/MOVSW) 大块的实现,但不足以超过 L1 缓存。
Intel Optimization Manual (2.5.6 REP String Enhancement) 提供了以下与 Nehalem 微架构相关的信息 - 2009 年和 2010 年发布的 Intel Core i5、i7 和 Xeon 处理器,以及后来的微架构,包括 Sandy Bridge 制造高达2013.
REP MOVSB
如果 ECX < 4,则 MOVSB 的延迟为 9 个周期。否则,ECX > 9 的 REP MOVSB 有 50 个周期的启动成本。
- tiny string (ECX < 4): REP MOVSB 延迟为 9 个周期;
- small string (ECX在4到9之间): Intel手册上没有官方资料,大概是9个周期以上,50个周期以下;
- 长字符串 (ECX > 9):50 个周期的启动成本。
MOVSW/MOVSD/MOVSQ
引自英特尔优化手册(2.5.6 REP 字符串增强):
- 短字符串(ECX <= 12):REP MOVSW/MOVSD/MOVSQ的延迟约为20个周期。
- 快速字符串(ECX >= 76:不包括 REP MOVSB):处理器实现通过在 16 字节中移动尽可能多的数据来提供硬件优化。如果 16 字节数据传输之一跨越高速缓存行边界,则 REP 字符串延迟的延迟会有所不同:
- = Split-free:延迟包括大约 40 个周期的启动成本,每 64 字节数据增加 4 个周期。
- = 缓存拆分:延迟包括大约 35 个周期的启动成本,每 64 字节数据增加 6 个周期。
- 中等字符串长度:REP MOVSW/MOVSD/MOVSQ 的延迟启动成本约为 15 个周期加上 word/dword/qword 中数据移动的每次迭代一个周期。
因此,根据 Intel 的说法,对于非常大的内存块,REP MOVSW 与 REP MOVSD/MOVSQ 一样快。无论如何,我的测试表明只有 REP MOVSD/MOVSQ 很快,而 REP MOVSW 在 Nehalem 和 Westmere 上比 REP MOVSB 还要慢。
根据英特尔在手册中提供的信息,在以前的英特尔微架构(2008 年之前)上,启动成本更高。
结论:如果只需要复制适合L1缓存的数据,复制64字节数据只需4个周期即可,不需要使用XMM寄存器!
#REP MOVSD/MOVSQ 是通用解决方案,如果数据适合 L1 缓存,它在所有 Intel 处理器上运行良好(不需要 ERMSB)#
以下是 REP MOVS* 的测试,当源和目标位于 L1 缓存中时,块足够大,不会受到启动成本的严重影响,但不会大到超过 L1 缓存大小。资料来源:http://users.atw.hu/instlatx64/
约娜 (2006-2008)
REP MOVSB 10.91 B/c
REP MOVSW 10.85 B/c
REP MOVSD 11.05 B/c
尼哈勒姆 (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
韦斯特米尔 (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(所有后续 CPUs 也具有增强的 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
天湖 (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 的测试结果只是为了确认 - 这些架构具有相同的每指令周期数据。
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
冰湖,移动版(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
Tremont,低功耗(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
Tiger Lake,移动版(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 工作得很好 - 你甚至不需要“Enhanced REP MOVSB”,因为在 Ivy Bridge (2013) 上使用 Enhacnced REP MOVSB,REP MOVSD 显示与 Nehalem (2010) 相同的字节数据每个时钟数据没有 增强的 REP MOVSB,而事实上 REP MOVSB 仅在 SkyLake (2015) 之后才变得非常快——是 Ivy Bridge 上的两倍。因此,CPUID 中的 Enhacnced REP MOVSB 位可能令人困惑 - 它仅表明 REP MOVSB
本身是可以的,但不是任何 REP MOVS*
更快。
最令人困惑的 ERMSB 实现是在 Ivy Bridge 微体系结构上。是的,在非常老的处理器上,在 ERMSB 之前,用于大块的 REP MOVS* 确实使用了常规代码不可用的缓存协议功能(无 RFO)。但是这个协议在有 ERMSB 的 Ivy Bridge 上不再使用。根据
关于 rep movsb
与 rep movsq
,在某些带有 ERMSB 的处理器上 rep movsb
稍快(例如 Xeon E3-1246 v3),在其他 rep movsq
上是更快(Skylake),而在其他设备上速度相同(例如 i7-1065G7)。但是,无论如何,我会选择 rep movsq
而不是 rep movsb
。
另请注意,此答案仅适用于源数据和目标数据适合 L1 缓存的情况。根据具体情况,应考虑内存访问(缓存等)的特殊性。 另请注意,此答案中的信息仅与英特尔处理器相关,与其他制造商(如 AMD)的处理器无关,这些处理器可能具有更好或更差的 REP MOVS* 指令实现。
Tinymembench 结果
这里有一些 tinymembench 结果显示 rep movsb
和 rep movsd
.
英特尔至强 E5-1650V3
Haswell 微架构,ERMS,AVX-2,2014 年 9 月发布,售价 583 美元,基本频率 3.5 GHz,最大睿频:3.8 GHz(单核),二级缓存 6 × 256 KB,三级缓存 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
Haswell、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
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
Kaby Lake,2017 年 3 月发布,售价 339 美元,基础频率 3.8 GHz,最大睿频 4.2 GHz,二级缓存 4 × 256 KB,三级缓存 8 MB,4 核(8 线程),4 RAM 模块 16384 MB DDR4 ECC 已安装,但只能使用 2 个内存通道。
MOVSB copy : 11720.8 MB/s
SSE2 fill : 15877.6 MB/s (2.7%)
SSE2 nontemporal fill : 36407.1 MB/s
英特尔 i7-1065G7
Ice Lake,AVX-512,ERMS,FSRM,1.37 GHz(在基本频率下工作,禁用涡轮模式)
MOVSB copy : 7322.7 MB/s
SSE2 fill : 9681.7 MB/s
SSE2 nontemporal fill : 16426.2 MB/s
AMD 霄龙 7401P
2017 年 6 月发布,售价 1075 美元,基于 Zen gen.1 微架构,24 核(48 线程),基本频率:2.0GHz,最大睿频:3.0GHz(少数核)或 2.8(所有核) ;高速缓存:L1 - 64 KB 实例。 & 每个核心 32 KB 数据,L2 - 每个核心 512 KB,L3 - 64 MB,每个 CCX 8 MB,DDR4-2666 8 通道,但只有 4 个 32768 MB 的 RAM 模块,每个 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
结论
REP MOVSD/MOVSQ 是通用解决方案,如果目标至少对齐 64 字节,则它在所有 Intel 处理器上对于至少 4KB 的大内存块(不需要 ERMSB)工作得相对较好。 REP MOVSD/MOVSQ 在更新的处理器上工作得更好,从 Skylake 开始。而且,对于 Ice Lake 或更新的微架构,它甚至可以完美地处理至少 64 字节的非常小的字符串。