当前C++编译器是否发出过"rep movsb/w/d"?
Does any of current C++ compilers ever emit "rep movsb/w/d"?
这个问题让我怀疑,当前的现代编译器是否会发出REP MOVSB/W/D
指令。
基于这一讨论,使用REP MOVSB/W/D
似乎对当前的CPU有益。
但无论我如何尝试,我都无法使当前的任何编译器(GCC 8、Clang 7、MSVC 2017和ICC 18)发出此指令。
对于这个简单的代码,发出REP MOVSB
:可能是合理的
void fn(char *dst, const char *src, int l) {
for (int i=0; i<l; i++) {
dst[i] = src[i];
}
}
但是编译器会发出一个未优化的简单字节复制循环,或者一个巨大的展开循环(基本上是内联的memmove
)。有编译器使用此指令吗?
GCC有x86调优选项来控制字符串操作策略以及何时内联与库调用。(请参见https://gcc.gnu.org/onlinedocs/gcc/x86-Options.html)。-mmemcpy-strategy=strategy
取alg:max_size:dest_align
三元组,但暴力方式为-mstringop-strategy=rep_byte
我不得不使用__restrict
让gcc识别memcpy模式,而不是在重叠检查/回退到哑字节循环后进行正常的自动向量化。(有趣的事实:即使使用-mno-sse
,gcc-O3也会使用整数寄存器的全宽自动向量化。因此,如果使用-Os
(针对大小进行优化)或-O2
(小于完全优化)进行编译,则只会得到一个哑字节循环)。
请注意,如果src和dst与dst > src
重叠,则结果为而不是memmove
。相反,您将得到一个长度为dst-src
的重复图案。即使在重叠的情况下,rep movsb
也必须正确地实现精确的字节复制语义,因此它仍然有效(但在当前CPU上速度较慢:我认为微码只会回到字节循环)。
gcc只能通过识别memcpy
模式,然后选择将memcpy内联为rep movsb
来获得rep movsb
它不会直接从字节复制循环到rep movsb
,这就是为什么可能的混叠会破坏优化。(不过,当别名分析无法证明它是memcpy或memmove时,-Os
可能会考虑在具有快速rep movsb
的CPU上直接使用rep movs
。)
void fn(char *__restrict dst, const char *__restrict src, int l) {
for (int i=0; i<l; i++) {
dst[i] = src[i];
}
}
这可能不应该"计数",因为除了"让编译器使用rep movs
"之外,我可能不会为任何用例推荐这些调优选项,所以它与内在的没有太大区别我没有检查所有的-mtune=silvermont
/-mtune=skylake
/-mtune=bdver2
(推土机版本2=Piledriver)等调整选项,但我怀疑其中任何一个都能启用。所以这是一个不切实际的测试,因为使用-march=native
的人都不会得到这个代码。
但是上面的C在Godbolt编译器资源管理器上使用gcc8.1-xc -O3 -Wall -mstringop-strategy=rep_byte -minline-all-stringops
编译到x86-64 System V:的asm
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
有趣的事实:为内联rep movs
而优化的x86-64 SysV调用约定并非巧合(为什么Windows64使用与x86-64上所有其他操作系统不同的调用约定?)。我认为gcc在设计调用约定时更喜欢这样,所以它保存了指令。
rep_8byte
做了一堆设置来处理不是8的倍数的计数,也许还有对齐,我没有仔细看。
我也没有检查其他编译器。
如果没有对齐保证,内嵌rep movsb
将是一个糟糕的选择,所以编译器默认情况下不这样做是好的。(只要他们做得更好。)英特尔的优化手册中有一节是关于memcpy和带有SIMD矢量的memset与rep movs
的比较。另请参阅http://agner.org/optimize/,以及x86标记wiki中的其他性能链接。
(我怀疑,如果你使用dst=__builtin_assume_aligned(dst, 64);
或任何其他与编译器通信对齐的方式,gcc是否会做任何不同的事情。例如,在一些数组上使用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检测设置的内部配置变量之后)。我不确定它实际会在哪些CPU上使用rep movsb
(如果有的话),但支持将其用于大型拷贝。
rep movsb
可能是一个非常合理的选择,适用于小代码大小和像这样的字节循环计数不可怕的缩放,并且可以安全地处理不太可能的重叠情况。
不过,对于当前CPU上通常较小的副本,使用微代码启动开销是一个大问题。
如果当前CPU上的平均拷贝大小可能为8到16个字节,和/或不同的计数会导致分支预测失误,那么它可能比字节循环更好。这不是好,但不那么糟糕。
如果在不进行自动向量化的情况下进行编译,那么将字节循环转换为rep movsb
的最后一搏窥视孔优化可能是个好主意(或者对于像MSVC这样的编译器,即使在完全优化的情况下也会进行字节循环。)
如果编译器更直接地了解它,并在为具有增强型Rep-Movs/Stos-Byte(ERMSB)功能的CPU进行调优时考虑将其用于-Os
(针对代码大小而非速度进行优化),那将是一件很好的事情。(另请参阅Enhanced REP MOVSB for memcpy,了解关于x86内存带宽单线程与所有内核、避免RFO的NT存储以及使用避免RFO缓存协议的rep movs
的许多好东西…)
在较旧的CPU上,rep movsb
不适合大拷贝,因此推荐的策略是rep movsd
或movsq
,对最后几个计数进行特殊处理。(假设您要使用rep movs
,例如在内核代码中,您不能触摸SIMD矢量寄存器。)
对于L1d或L2高速缓存中热的中等大小的副本,使用整数寄存器的-mno-sse
自动矢量化比rep movs
差得多,因此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 if profile guided优化将优化gcc的策略,例如,当每次调用的计数很小时,倾向于更小/更简单的代码,因此无法实现自动矢量化代码。或者,如果代码是"冷"的,并且在整个程序的每次运行中只运行一次或根本不运行,则更改策略。或者,如果计数通常是16或24左右,那么最后一个n % 32
字节的标量就很糟糕,所以理想情况下PGO会将其设置为特殊情况下的较小计数。(但我并不太乐观。)
我可能会报告一个GCC遗漏的优化错误,关于在重叠检查后检测memcpy,而不是将其完全留给自动矢量器。和/或关于使用rep movs
代替-Os
,如果有更多关于该uarch的信息可用,可以使用-mtune=icelake
。
许多软件都是用-O2
编译的,所以rep movs
的窥视孔而不是自动矢量器可能会有所不同。(但问题是这是一个积极的还是消极的差异)!
- 防止gcc破坏我的AVX2内部复制到REP MOVS
- 当前C++编译器是否发出过"rep movsb/w/d"?
- 尝试转换 std::chrono::d uration 会导致"rep cannot be a duration"编译错误
- 使用 zmq::p roxy 和 REQ/REP 模式
- 为什么 zmq REQ-REP 不起作用?
- EAGAIN 在 zeromq REQ/REP 上收到,没有阻塞套接字
- 为什么ZeroMQ req rep在C++和Python之间给我一个空响应
- ZeroMQ REQ/REP如何处理多个客户端
- 在 32 位和 64 位程序中使用 std::chrono::d uration::rep 和 printf
- C++ 具有 REQ 和 REP 套接字的 ZeroMQ 单一应用程序
- 来自 Visual Studio 的此代码中的 rep stos 程序集命令的目的
- zeromq:重置REQ/REP套接字状态
- 带有大型消息的ZeroMQ:REQ/REP
- 将minutes::rep转换为hours::rep
- c++ req-rep终止错误
- 将ZeroMQ REQ/REP与C++11期货混合