GCC 具有 x86 调整选项来控制字符串操作策略以及何时内联与库调用。 (看https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html). -mmemcpy-strategy=strategy
takes alg:max_size:dest_align
三胞胎,但蛮力方法是-mstringop-strategy=rep_byte
我不得不使用__restrict
让 gcc 识别 memcpy 模式,而不是在重叠检查/回退到哑字节循环后进行正常的自动矢量化。 (有趣的事实:gcc -O3 即使使用-mno-sse
,使用整数寄存器的全宽度。所以如果你编译的话你只会得到一个愚蠢的字节循环-Os
(优化尺寸)或-O2
(低于完全优化))。
请注意,如果 src 和 dst 重叠dst > src
,结果是not memmove
。相反,您将得到长度=的重复模式dst-src
. rep movsb
即使在重叠的情况下,也必须正确实现确切的字节复制语义,因此它仍然有效(但在当前的 CPU 上速度很慢:我认为微代码只会退回到字节循环)。
gcc 只能到达rep movsb
通过识别一个memcpy
模式,然后选择内联 memcpy 作为rep movsb
.它不会直接从字节复制循环转到rep movsb
,这就是为什么可能的混叠会导致优化失效。 (这可能很有趣-Os
考虑使用rep movs
但是,当别名分析无法证明它是 memcpy 或 memmove 时,在具有快速的 CPU 上直接进行rep movsb
.)
void fn(char *__restrict dst, const char *__restrict src, int l) {
for (int i=0; i<l; i++) {
dst[i] = src[i];
}
}
这可能不应该“计数”,因为我可能会not推荐这些调整选项用于除“让编译器使用rep movs
”,所以它与内在函数没有什么不同。我没有检查所有-mtune=silvermont
/ -mtune=skylake
/ -mtune=bdver2
(推土机版本 2 = 打桩机)/等等调整选项,但我怀疑其中任何一个都可以实现这一点。所以这是一个不切实际的测试,因为没有人使用-march=native
会得到这个代码生成。
但是上面的C编译与 gcc8.1-xc -O3 -Wall -mstringop-strategy=rep_byte -minline-all-stringops在 Godbolt 编译器资源管理器上将此 asm 用于 x86-64 System V:
fn:
test edx, edx
jle .L1 # rep movs treats the counter as unsigned, but the source uses signed
sub edx, 1 # what the heck, gcc? mov ecx,edx would be too easy?
lea ecx, [rdx+1]
rep movsb # dst=rdi and src=rsi
.L1: # matching the calling convention
ret
有趣的事实:x86-64 SysV 调用约定针对内联进行了优化rep movs
不是巧合(为什么 Windows64 使用与 x86-64 上所有其他操作系统不同的调用约定?)。我认为 gcc 在设计调用约定时倾向于这样做,因此它保存了指令。
rep_8byte
does a bunch设置来处理不是8的倍数的计数,也许还有对齐,我没有仔细看。
我也没有检查其他编译器。
内联rep movsb
如果没有对齐保证,这将是一个糟糕的选择,因此编译器默认情况下不这样做是件好事。 (只要他们这样做某物更好的。) Intel优化手册有一节介绍了带有 SIMD 向量的 memcpy 和 memset 与 SIMD 向量对比。rep movs
。也可以看看http://agner.org/optimize/,以及其他性能链接x86 标签维基.
(我怀疑如果你这样做的话 gcc 会做任何不同的事情dst=__builtin_assume_aligned(dst, 64);
或者任何其他与编译器通信对齐的方式。例如alignas(64)
在某些阵列上。)
英特尔的 IceLake 微架构将具有“短重复”功能,可能会减少启动开销rep movs
/ rep stos
,使它们对于小计数更有用。 (现在rep
字符串微码具有显着的启动开销:REP 执行什么设置?)
memmove / memcpy 策略:
顺便说一句,glibc 的 memcpy 对小输入使用了一个非常好的策略,该策略对重叠不敏感:两个加载 -> 两个可能重叠的存储,最多复制 2 个寄存器宽。例如,这意味着来自 4..7 字节的任何输入都会以相同的方式分支。
Glibc 的 asm 源代码有一个很好的评论描述了该策略:https://code.woboq.org/userspace/glibc/sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S.html#19.
对于大输入,它使用 SSE XMM 寄存器、AVX YMM 寄存器或rep movsb
(在检查 glibc 初始化自身时基于 CPU 检测设置的内部配置变量之后)。我不确定它实际会使用哪些 CPUrep movsb
开启(如果有),但支持将其用于大型副本。
rep movsb
对于小代码大小和不可怕的缩放,对于像这样的字节循环的计数来说,可能是一个相当合理的选择,对不太可能发生的重叠情况进行安全处理。
不过,在当前的 CPU 上,将微代码用于通常较小的副本时,微代码启动开销是一个大问题。
如果当前 CPU 上的平均副本大小可能为 8 到 16 字节,并且/或不同的计数导致分支预测错误很多,那么它可能比字节循环更好。它不是good,但还好。
某种最后的窥视孔优化,用于将字节循环变成rep movsb
如果在不进行自动矢量化的情况下进行编译,这可能是一个好主意。(或者对于像 MSVC 这样的编译器,即使在完全优化的情况下也会产生字节循环。)
如果编译器更直接地了解它并考虑将它用于-Os
使用增强型 Rep Movs/Stos Byte (ERMSB) 功能调整 CPU 时(针对代码大小进行优化而不是针对速度进行优化)。 (也可以看看memcpy 的增强型 REP MOVSB有关 x86 内存带宽单线程与所有核心、避免 RFO 的 NT 存储的许多好东西,以及rep movs
使用避免 RFO 的缓存协议...)。
在较旧的 CPU 上,rep movsb
对于大型副本来说不太好,所以推荐的策略是rep movsd
or movsq
对最后几个计数进行特殊处理。 (假设你要使用rep movs
根本,例如在内核代码中,您无法触及 SIMD 向量寄存器。)
The -mno-sse
使用整数寄存器的自动向量化比rep movs
对于在 L1d 或 L2 缓存中很热的中型副本,因此 gcc 绝对应该使用rep movsb
or rep movsq
检查重叠后,不是 qword 复制循环,除非它期望小输入(如 64 字节)是常见的。
字节循环的唯一优点是代码尺寸小;它几乎是桶的底部;对于较小但未知的副本大小,像 glibc 这样的智能策略会更好。但是内联的代码太多了,并且函数调用确实有一些成本(溢出调用破坏的寄存器并破坏红色区域,加上call
/ ret
指令和动态链接间接)。
特别是在不经常运行的“冷”函数中(因此您不想在其上花费大量代码大小,增加程序的 I-cache 占用空间、TLB 局部性、从磁盘加载的页面等) 。如果手动编写汇编,您通常会更多地了解预期的大小分布,并且能够内联快速路径并回退到其他内容。
请记住,编译器将对一个程序中可能存在的多个循环做出决定,并且大多数程序中的大多数代码都在热循环之外。它不应该让它们全部膨胀。这就是为什么 gcc 默认为-fno-unroll-loops
除非启用了配置文件引导优化。 (自动矢量化启用于-O3
但是,并且可以为像这样的一些小循环创建大量代码。这是非常愚蠢的,gcc 在循环序言/结尾处花费了大量的代码大小,但在实际循环上花费了很少的代码大小;据它所知,每次外部代码运行时,循环都会运行数百万次迭代。)
不幸的是,gcc 的自动矢量化代码并不非常高效或紧凑。它在 16 字节 SSE 情况的循环清理代码上花费了大量代码(完全展开 15 字节副本)。使用 32 字节 AVX 向量,我们得到一个汇总字节loop来处理剩余的元素。 (对于 17 字节副本,这与 1 XMM 向量 + 1 字节或 glibc 样式重叠 16 字节副本相比非常糟糕)。对于 gcc7 及更早版本,它会执行相同的完全展开,直到对齐边界作为循环序言,因此它会膨胀两倍。
IDK 如果配置文件引导优化可以优化 gcc 的策略,例如当每次调用的计数都很小时,倾向于更小/更简单的代码,因此无法达到自动向量化代码。或者,如果代码“冷”并且每次运行整个程序只运行一次或根本不运行,则更改策略。或者,如果计数通常为 16 或 24 等,则最后一个的标量n % 32
bytes 很糟糕,所以理想情况下 PGO 会将其转换为特殊情况下较小的计数。 (但我不太乐观。)
我可能会为此报告一个 GCC 错过优化的错误,即在重叠检查后检测 memcpy,而不是完全将其留给自动矢量化器。和/或关于使用rep movs
for -Os
,也许与-mtune=icelake
如果有关于该 uarch 的更多信息。
很多软件只用编译-O2
,所以有一个窥视孔rep movs
除了自动矢量化器之外,其他功能也可能有所不同。 (但问题是是正差还是负差)!