MOVSD 性能取决于参数
MOVSD performance depends on arguments
我刚刚注意到我的一段代码在复制内存时表现出不同的性能。一项测试表明,如果目标缓冲区的地址大于源地址,内存复制性能会下降。听起来很荒谬,但下面的代码显示了差异 (Delphi):
const MEM_CHUNK = 50 * 1024 * 1024;
ROUNDS_COUNT = 100;
LpSrc := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);
LpDest := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);
QueryPerformanceCounter(LTick1);
for i := 0 to ROUNDS_COUNT - 1 do
CopyMemory(LpDest,LpSrc,MEM_CHUNK);
QueryPerformanceCounter(LTick2);
// show timings
QueryPerformanceCounter(LTick1);
for i := 0 to ROUNDS_COUNT - 1 do
CopyMemory(LpSrc,LpDest,MEM_CHUNK);
QueryPerformanceCounter(LTick2);
// show timings
这里CopyMemory是基于MOVSD的。结果:
Starting Memory Bandwidth Test...
LpSrc 0x06FC0000
LpDest 0x0A1C0000
src->dest Transfer: 5242880000 bytes in 1,188 sec @4,110 GB/s.
dest->src Transfer: 5242880000 bytes in 0,805 sec @6,066 GB/s.
src->dest Transfer: 5242880000 bytes in 1,142 sec @4,275 GB/s.
dest->src Transfer: 5242880000 bytes in 0,832 sec @5,871 GB/s.
在两个系统上试过,无论重复多少次,结果都是一致的。
从来没有见过这样的东西。无法 google 它。这是已知行为吗?这只是另一个与缓存相关的特性吗?
更新:
以下是页面对齐缓冲区和 MOVSD (DF=0) 正向的最终结果:
Starting Memory Bandwidth Test...
LpSrc 0x06F70000
LpDest 0x0A170000
src->dest Transfer: 5242880000 bytes in 0,781 sec @6,250 GB/s.
dest->src Transfer: 5242880000 bytes in 0,731 sec @6,676 GB/s.
src->dest Transfer: 5242880000 bytes in 0,750 sec @6,510 GB/s.
dest->src Transfer: 5242880000 bytes in 0,735 sec @6,640 GB/s.
src->dest Transfer: 5242880000 bytes in 0,742 sec @6,585 GB/s.
dest->src Transfer: 5242880000 bytes in 0,750 sec @6,515 GB/s.
... and so on.
这里的传输速率是恒定的。
通常 fast-strings 或 微代码使 rep movsb/w/d/q
和 rep stosb/w/d/q
快速进行大量计数(复制 16、32 甚至 64 字节块)。并且可能与商店的 RFO-avoiding 协议。 (其他 repe/repne scas/cmps
总是很慢)。
某些输入条件会干扰 best-case,特别是 DF=1(向后)而不是正常的 DF=0。
rep movsd
性能可能取决于 src 和 dst 的对齐,包括它们的 relative 未对齐。显然,两个指针都 = 32*n + same
并不算太糟糕,因此大部分复制都可以在到达对齐边界后完成。 (绝对错位,但指针相对于彼此对齐。即 dst-src
是 32 或 64 字节的倍数)。
性能 不 取决于 src > dst
或 src < dst
per-se。如果指针在重叠的 16 或 32 字节范围内,那也可以强制 fall-back 一次到 1 个元素。
Intel 的优化手册中有一节是关于 memcpy 实现和比较 rep movs
与 well-optimized SIMD 循环的。启动开销是 rep movs
最大的缺点之一,但它不能很好地处理的错位也是如此。 (IceLake 的 "fast short rep
" 功能大概解决了这个问题。)
I did not disclose the CopyMemory body - and it indeed used copying backwards (df=1) when avoiding overlaps.
是的,这是你的问题。如果您需要避免实际重叠,则仅向后复制,而不仅仅是基于哪个地址更高。然后用 SIMD 向量来做,而不是 rep movsd
.
rep movsd
只有在 DF=0(升序地址)时才快,至少在 Intel CPUs 上是这样。 我刚刚检查了 Skylake:1000000使用 rep movsb
从 page-aligned 缓冲区复制 4096 non-overlapping 字节的次数在:
中运行
- 174M 周期,
cld
(DF=0 转发)。在大约 4.1GHz 时大约 42ms,或达到大约 90GiB/s L1d 读+写带宽。每个周期大约 23 个字节,因此每个 rep movsb
的启动开销似乎正在伤害我们。在这种纯 L1d 缓存命中的简单情况下,AVX 复制循环应该达到接近 32B/s,即使在从内部循环退出循环时出现分支预测错误。
- 4161M 循环
std
(DF=1 向后)。大约 4.1GHz 时大约 1010 毫秒,或大约 3.77GiB/s 读+写。大约 0.98 字节/周期,与 rep movsb
完全一致 un-optimized。 (每个周期 1 个计数,因此 rep movsd
大约是缓存命中带宽的 4 倍。)
uops_executed
perf counter 还确认它在向后复制时花费了更多的微指令。 (这是在 Linux 下长模式下的 dec ebp / jnz
循环内。与使用 NASM 构建的 相同的测试循环,使用 BSS 中的缓冲区。循环做了 cld
或 std
/ 2x lea
/ mov ecx, 4096
/ rep movsb
。将 cld
提升到循环外并没有太大区别。)
您使用的是一次复制 4 个字节的 rep movsd
,因此对于向后复制,如果它们命中缓存,我们可以预期 4 个字节/周期。而且您可能正在使用大缓冲区,因此缓存会错过向前方向的瓶颈,不会比向后快多少。但是来自反向复制的额外 uops 会损害内存并行性:适合 out-of-order window 的加载 uops 触及的缓存行更少。此外,在 Intel CPUs 中,某些预取器向后工作的效果较差。 L2 流光在任一方向都有效,但我认为 L1d 预取只会向前。
相关: 你的 Sandybridge 对于 ERMSB 来说太旧了,但是 rep movs
/rep stos
的 Fast Strings 自最初的 P6 以来就已经存在了。以今天的标准来看,您 2006 年的 Clovertown Xeon 已经相当古老了。 (Conroe/Merom 微架构)。与今天的 many-core Xeon 不同,那些 CPU 可能太旧了,以至于 Xeon 的单个内核可以饱和微薄的内存带宽。
我的缓冲区是 page-aligned。对于向下,我尝试让初始 RSI/RDI 指向页面的最后一个字节,因此初始指针未对齐,但要复制的总区域是对齐的。我也试过 lea rdi, [buf+4096]
所以起始指针是 page-aligned,所以 [buf+0]
没有被写入。两者都没有使向后复制更快; rep movs
只是 DF=1 的垃圾;如果需要向后复制,请使用 SIMD 向量。
通常 SIMD 向量循环至少可以和 rep movs
一样快,前提是您可以使用机器支持的尽可能宽的向量。这意味着拥有 SSE、AVX 和 AVX512 版本......在没有运行时分派到针对特定 CPU、rep movsd
调整的 memcpy
实现的可移植代码中通常非常好,应该是在未来 CPU 上会更好,例如 IceLake。
您实际上不需要页面对齐来使 rep movs
变快。 IIRC,32 字节对齐的源和目标就足够了。但 4k 别名也可能是一个问题:如果 dst & 4095
略高于 src & 4095
,加载微指令可能在内部必须等待一些额外的周期来存储微指令,因为 fast-path 检测机制当加载重新加载时,最近的存储只查看 page-offset 位。
页面对齐是确保您获得最佳案例的一种方法rep movs
,不过
通常情况下,您可以从 SIMD 循环中获得最佳性能,但前提是您使用机器支持的 SIMD 矢量(如 AVX,甚至可能是 AVX512)。你应该根据硬件和周围的代码选择 NT 商店还是普通商店。
我刚刚注意到我的一段代码在复制内存时表现出不同的性能。一项测试表明,如果目标缓冲区的地址大于源地址,内存复制性能会下降。听起来很荒谬,但下面的代码显示了差异 (Delphi):
const MEM_CHUNK = 50 * 1024 * 1024;
ROUNDS_COUNT = 100;
LpSrc := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);
LpDest := VirtualAlloc(0,MEM_CHUNK,MEM_COMMIT,PAGE_READWRITE);
QueryPerformanceCounter(LTick1);
for i := 0 to ROUNDS_COUNT - 1 do
CopyMemory(LpDest,LpSrc,MEM_CHUNK);
QueryPerformanceCounter(LTick2);
// show timings
QueryPerformanceCounter(LTick1);
for i := 0 to ROUNDS_COUNT - 1 do
CopyMemory(LpSrc,LpDest,MEM_CHUNK);
QueryPerformanceCounter(LTick2);
// show timings
这里CopyMemory是基于MOVSD的。结果:
Starting Memory Bandwidth Test...
LpSrc 0x06FC0000
LpDest 0x0A1C0000
src->dest Transfer: 5242880000 bytes in 1,188 sec @4,110 GB/s.
dest->src Transfer: 5242880000 bytes in 0,805 sec @6,066 GB/s.
src->dest Transfer: 5242880000 bytes in 1,142 sec @4,275 GB/s.
dest->src Transfer: 5242880000 bytes in 0,832 sec @5,871 GB/s.
在两个系统上试过,无论重复多少次,结果都是一致的。
从来没有见过这样的东西。无法 google 它。这是已知行为吗?这只是另一个与缓存相关的特性吗?
更新:
以下是页面对齐缓冲区和 MOVSD (DF=0) 正向的最终结果:
Starting Memory Bandwidth Test...
LpSrc 0x06F70000
LpDest 0x0A170000
src->dest Transfer: 5242880000 bytes in 0,781 sec @6,250 GB/s.
dest->src Transfer: 5242880000 bytes in 0,731 sec @6,676 GB/s.
src->dest Transfer: 5242880000 bytes in 0,750 sec @6,510 GB/s.
dest->src Transfer: 5242880000 bytes in 0,735 sec @6,640 GB/s.
src->dest Transfer: 5242880000 bytes in 0,742 sec @6,585 GB/s.
dest->src Transfer: 5242880000 bytes in 0,750 sec @6,515 GB/s.
... and so on.
这里的传输速率是恒定的。
通常 fast-strings 或 rep movsb/w/d/q
和 rep stosb/w/d/q
快速进行大量计数(复制 16、32 甚至 64 字节块)。并且可能与商店的 RFO-avoiding 协议。 (其他 repe/repne scas/cmps
总是很慢)。
某些输入条件会干扰 best-case,特别是 DF=1(向后)而不是正常的 DF=0。
rep movsd
性能可能取决于 src 和 dst 的对齐,包括它们的 relative 未对齐。显然,两个指针都 = 32*n + same
并不算太糟糕,因此大部分复制都可以在到达对齐边界后完成。 (绝对错位,但指针相对于彼此对齐。即 dst-src
是 32 或 64 字节的倍数)。
性能 不 取决于 src > dst
或 src < dst
per-se。如果指针在重叠的 16 或 32 字节范围内,那也可以强制 fall-back 一次到 1 个元素。
Intel 的优化手册中有一节是关于 memcpy 实现和比较 rep movs
与 well-optimized SIMD 循环的。启动开销是 rep movs
最大的缺点之一,但它不能很好地处理的错位也是如此。 (IceLake 的 "fast short rep
" 功能大概解决了这个问题。)
I did not disclose the CopyMemory body - and it indeed used copying backwards (df=1) when avoiding overlaps.
是的,这是你的问题。如果您需要避免实际重叠,则仅向后复制,而不仅仅是基于哪个地址更高。然后用 SIMD 向量来做,而不是 rep movsd
.
rep movsd
只有在 DF=0(升序地址)时才快,至少在 Intel CPUs 上是这样。 我刚刚检查了 Skylake:1000000使用 rep movsb
从 page-aligned 缓冲区复制 4096 non-overlapping 字节的次数在:
- 174M 周期,
cld
(DF=0 转发)。在大约 4.1GHz 时大约 42ms,或达到大约 90GiB/s L1d 读+写带宽。每个周期大约 23 个字节,因此每个rep movsb
的启动开销似乎正在伤害我们。在这种纯 L1d 缓存命中的简单情况下,AVX 复制循环应该达到接近 32B/s,即使在从内部循环退出循环时出现分支预测错误。 - 4161M 循环
std
(DF=1 向后)。大约 4.1GHz 时大约 1010 毫秒,或大约 3.77GiB/s 读+写。大约 0.98 字节/周期,与rep movsb
完全一致 un-optimized。 (每个周期 1 个计数,因此rep movsd
大约是缓存命中带宽的 4 倍。)
uops_executed
perf counter 还确认它在向后复制时花费了更多的微指令。 (这是在 Linux 下长模式下的 dec ebp / jnz
循环内。与使用 NASM 构建的 cld
或 std
/ 2x lea
/ mov ecx, 4096
/ rep movsb
。将 cld
提升到循环外并没有太大区别。)
您使用的是一次复制 4 个字节的 rep movsd
,因此对于向后复制,如果它们命中缓存,我们可以预期 4 个字节/周期。而且您可能正在使用大缓冲区,因此缓存会错过向前方向的瓶颈,不会比向后快多少。但是来自反向复制的额外 uops 会损害内存并行性:适合 out-of-order window 的加载 uops 触及的缓存行更少。此外,在 Intel CPUs 中,某些预取器向后工作的效果较差。 L2 流光在任一方向都有效,但我认为 L1d 预取只会向前。
相关:rep movs
/rep stos
的 Fast Strings 自最初的 P6 以来就已经存在了。以今天的标准来看,您 2006 年的 Clovertown Xeon 已经相当古老了。 (Conroe/Merom 微架构)。与今天的 many-core Xeon 不同,那些 CPU 可能太旧了,以至于 Xeon 的单个内核可以饱和微薄的内存带宽。
我的缓冲区是 page-aligned。对于向下,我尝试让初始 RSI/RDI 指向页面的最后一个字节,因此初始指针未对齐,但要复制的总区域是对齐的。我也试过 lea rdi, [buf+4096]
所以起始指针是 page-aligned,所以 [buf+0]
没有被写入。两者都没有使向后复制更快; rep movs
只是 DF=1 的垃圾;如果需要向后复制,请使用 SIMD 向量。
通常 SIMD 向量循环至少可以和 rep movs
一样快,前提是您可以使用机器支持的尽可能宽的向量。这意味着拥有 SSE、AVX 和 AVX512 版本......在没有运行时分派到针对特定 CPU、rep movsd
调整的 memcpy
实现的可移植代码中通常非常好,应该是在未来 CPU 上会更好,例如 IceLake。
您实际上不需要页面对齐来使 rep movs
变快。 IIRC,32 字节对齐的源和目标就足够了。但 4k 别名也可能是一个问题:如果 dst & 4095
略高于 src & 4095
,加载微指令可能在内部必须等待一些额外的周期来存储微指令,因为 fast-path 检测机制当加载重新加载时,最近的存储只查看 page-offset 位。
页面对齐是确保您获得最佳案例的一种方法rep movs
,不过
通常情况下,您可以从 SIMD 循环中获得最佳性能,但前提是您使用机器支持的 SIMD 矢量(如 AVX,甚至可能是 AVX512)。你应该根据硬件和周围的代码选择 NT 商店还是普通商店。