gez*_*eza 3 c++ x86 assembly micro-optimization compiler-optimization
这个问题让我想知道,当前的现代编译器是否曾经发出REP MOVSB/W/D指令。
基于此讨论,似乎REP MOVSB/W/D在当前 CPU 上使用可能是有益的。
但无论我如何尝试,我都无法让当前的任何编译器(GCC 8、Clang 7、MSVC 2017 和 ICC 18)发出这条指令。
对于这个简单的代码,emit 可能是合理的REP MOVSB:
void fn(char *dst, const char *src, int l) {
for (int i=0; i<l; i++) {
dst[i] = src[i];
}
}
Run Code Online (Sandbox Code Playgroud)
但是编译器会发出一个未优化的简单字节复制循环,或者一个巨大的展开循环(基本上是内联的memmove)。是否有任何编译器使用此指令?
GCC 具有 x86 调整选项来控制字符串操作策略以及何时内联与库调用。(参见https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)。 -mmemcpy-strategy=strategy
需要alg:max_size:dest_align三胞胎,但蛮力方式是-mstringop-strategy=rep_byte
我不得不使用__restrictgcc 来识别 memcpy 模式,而不是在重叠检查/回退到哑字节循环后仅进行正常的自动矢量化。(有趣的事实:gcc -O3 甚至-mno-sse使用整数寄存器的全宽自动矢量化。因此,如果您使用-Os(优化大小)或-O2(未完全优化)编译,您只会得到一个愚蠢的字节循环。
请注意,如果 src 和 dst 与 重叠dst > src,则结果不是 memmove。相反,你会得到一个重复的模式,长度为 = dst-src。 rep movsb即使在重叠的情况下也必须正确实现精确的字节复制语义,因此它仍然有效(但在当前的 CPU 上很慢:我认为微码会回退到字节循环)。
gcc 只能rep movsb通过识别memcpy模式然后选择将 memcpy 内联为rep movsb. 它不会直接从字节复制循环到rep movsb,这就是可能的别名使优化失败的原因。(不过,当别名分析无法证明它是 memcpy 或 memmove 时,在具有 fast 的 CPU 上-Os考虑rep movs直接使用可能会很有趣rep movsb。)
void fn(char *__restrict dst, const char *__restrict src, int l) {
for (int i=0; i<l; i++) {
dst[i] = src[i];
}
}
Run Code Online (Sandbox Code Playgroud)
这可能不应该“计数”,因为除了“使编译器使用”之外,我可能不会为任何用例推荐这些调整选项rep movs,所以它与内在的没有什么不同。 我没有检查所有-mtune=silvermont / -mtune=skylake/ -mtune=bdver2(Bulldozer version 2 = Piledriver)/ 等调整选项,但我怀疑它们中的任何一个都启用了该选项。所以这是一个不切实际的测试,因为没有人使用-march=native会得到这个代码生成。
但上述C编译与gcc8.1-xc -O3 -Wall -mstringop-strategy=rep_byte -minline-all-stringops对Godbolt编译器Explorer来此ASM为x86-64的系统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
Run Code Online (Sandbox Code Playgroud)
有趣的事实:针对内联优化的 x86-64 SysV 调用约定rep movs并非巧合(为什么 Windows64 使用与 x86-64 上的所有其他操作系统不同的调用约定?)。我认为 gcc 在设计调用约定时更喜欢这样做,因此它节省了指令。
rep_8byte做了一堆设置来处理不是 8 倍数的计数,也许是对齐,我没有仔细看。
我也没有检查其他编译器。
rep movsb如果没有对齐保证,内联将是一个糟糕的选择,因此编译器默认不这样做是件好事。(只要他们做的东西比较好。) 英特尔优化手册与SIMD向量与上memcpy和memset的一个部分rep movs。另请参阅http://agner.org/optimize/以及x86 标记 wiki中的其他性能链接。
(我怀疑 gcc 会做任何不同的事情,如果你这样做了,dst=__builtin_assume_aligned(dst, 64);或者以任何其他方式与编译器通信。例如,alignas(64)在某些数组上。)
Intel 的 IceLake 微架构将具有“short rep”功能,大概可以减少rep movs/ 的启动开销rep stos,使它们对于小计数更有用。(目前rep字符串微码有很大的启动开销:REP 做了什么设置?)
顺便说一句,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(在检查基于 CPU 检测在 glibc 初始化自身时设置的内部配置变量之后)。我不确定它将实际使用哪些 CPU rep movsb(如果有的话),但支持将其用于大副本。
rep movsb对于像这样的字节循环计数较小的代码大小和非可怕的缩放,可能是一个非常合理的选择,并且对不太可能的重叠情况进行安全处理。
微码启动开销是一个大问题,将它用于当前 CPU 上通常很小的副本。
如果当前 CPU 上的平均副本大小可能是 8 到 16 字节,和/或不同的计数会导致分支预测错误很多,那么它可能比字节循环更好。这不是很好,但也没有那么糟糕。
rep movsb如果在没有自动矢量化的情况下进行编译,则将字节循环转换为字节循环的某种最后一搏窥视孔优化可能是一个好主意。 (或者对于像 MSVC 这样的编译器,即使在完全优化时也会进行字节循环。)
如果编译器更直接地了解它,并在使用-Os增强型 Rep Movs/Stos Byte (ERMSB) 功能调整 CPU 时考虑将其用于(优化代码大小而不是速度),那就太好了。(另请参阅 增强型 REP MOVSB for memcpy,了解有关 x86 内存带宽单线程与所有内核、避免 RFO 的 NT 存储以及rep movs使用避免 RFO 的缓存协议的许多好东西......)。
在较旧的 CPU 上,rep movsb对于大副本来说不是那么好,因此推荐的策略是rep movsd或movsq对最后几个计数进行特殊处理。(假设您将完全使用rep movs,例如在您无法触及 SIMD 向量寄存器的内核代码中。)
-mno-sse使用整数寄存器的自动向量化比rep movs在 L1d 或 L2 缓存中很热的中等大小的副本要差得多,因此 gcc 绝对应该使用rep movsb或rep movsq在检查重叠后使用,而不是 qword 复制循环,除非它期望小输入(如 64 字节) ) 成为普遍现象。
字节循环的唯一优点是代码量小;它几乎是桶的底部;对于小但未知的副本大小,像 glibc 这样的智能策略会更好。但内联代码太多,函数调用确实有一些成本(溢出调用破坏的寄存器和破坏红色区域,加上call/ret指令和动态链接间接的实际成本)。
特别是在不经常运行的“冷”函数中(因此您不想在其上花费大量代码大小、增加程序的 I-cache 占用空间、TLB 位置、要从磁盘加载的页面等) . 如果手动编写 asm,您通常会更多地了解预期的大小分布,并且能够内联快速路径并回退到其他内容。
请记住,编译器将在一个程序中对潜在的多个循环做出决定,并且大多数程序中的大多数代码都在热循环之外。它不应该使它们全部膨胀。 这就是 gcc 默认为-fno-unroll-loops除非启用配置文件引导优化的原因。(-O3虽然在 启用了自动矢量化,并且可以为一些像这样的小循环创建大量代码。gcc 在循环序言/尾声上花费大量代码大小是非常愚蠢的,但在实际中花费很少循环;因为它知道,每次外部代码运行时,循环都会运行数百万次迭代。)
不幸的是,它不像 gcc 的自动矢量化代码非常高效或紧凑。对于 16 字节 SSE 情况(完全展开 15 字节副本),它在循环清理代码上花费了大量代码大小。使用 32 字节的 AVX 向量,我们得到了一个卷起的字节循环来处理剩余的元素。(对于 17 字节副本,这与 1 XMM 向量 + 1 字节或 glibc 样式重叠 16 字节副本相比非常糟糕)。使用 gcc7 及更早版本,它会执行相同的完全展开,直到对齐边界作为循环序言,因此它会膨胀两倍。
IDK 如果配置文件引导的优化将在这里优化 gcc 的策略,例如,当每次调用的计数很小时,支持更小/更简单的代码,因此不会达到自动矢量化代码。或者如果代码是“冷的”并且每次运行整个程序只运行一次或根本不运行,则更改策略。或者,如果计数通常是 16 或 24 或其他什么,那么最后一个n % 32字节的标量很糟糕,所以理想情况下 PGO 会将其设置为特殊情况下较小的计数。(但我不太乐观。)
我可能会为此报告一个 GCC 遗漏优化错误,关于在重叠检查后检测 memcpy,而不是将它完全留给自动矢量化器。和/或关于使用rep movsfor -Os,-mtune=icelake如果有关该 uarch 的更多信息可用,可能会使用。
许多软件仅使用 进行编译-O2,因此rep movs自动矢量化器以外的窥视孔可能会有所作为。(但问题是它是正差异还是负差异)!
| 归档时间: |
|
| 查看次数: |
709 次 |
| 最近记录: |