gpt4 book ai didi

c++ - 当前的任何C++编译器都发出 “rep movsb/w/d”吗?

转载 作者:行者123 更新时间:2023-12-02 12:19:25 29 4
gpt4 key购买 nike

这个question让我想知道,当前的现代编译器是否曾经发出REP MOVSB/W/D指令。

基于此discussion,似乎在当前CPU上使用REP MOVSB/W/D可能会有所帮助。

但是无论如何尝试,我都无法使任何当前的编译器(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模式,而不是在重叠检查/回退到哑字节循环后仅执行正常的自动矢量化。 (有趣的事实: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时,对于使用-Os快速的CPU,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];
}
}

这可能不应该“计数”,因为除了“使编译器使用rep movs”之外,我可能不建议针对任何用例的那些调整选项,因此它与内部函数没有什么不同。 我没有检查所有的 -mtune=silvermont / -mtune=skylake / -mtune=bdver2(Bulldozer版本2 =打桩机)/等等调优选项,但是我怀疑它们中的任何一个都启用了该功能。因此,这是不切实际的测试,因为没有人使用 -march=native会获得此代码源。

但是,上面的C在Godbolt编译器资源管理器中将 with 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调用约定不是巧合( Why does Windows64 use a different calling convention from all other OSes on x86-64?)。我认为gcc支持在设计调用约定时使用它,因此节省了指令。
rep_8byte做了很多设置来处理不是8的倍数的计数,也许是对齐,我没有仔细看。

我也没有检查其他编译器。

如果没有对齐保证,则内联rep movsb将是一个糟糕的选择,因此,编译器默认情况下不执行此操作是一件好事。 (只要它们做得更好。) Intel's optimization manual包含关于memcpy和memset的部分,其中包含SIMD vector 和 rep movs。另请参见 http://agner.org/optimize/the x86 tag wiki中的其他性能链接。

(我怀疑gcc如果执行 dst=__builtin_assume_aligned(dst, 64);或任何其他将对齐方式传达给编译器的方式会做任何不同的事情,例如某些数组上的 alignas(64)。)

英特尔的IceLake微体系结构将具有“短重复”功能,可以减少 rep movs / rep stos的启动开销,从而使它们在小批量生产时更加有用。 (当前 rep字符串微码的启动开销很大: What setup does REP do?)

记忆/记忆策略:

顺便说一句,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(优化代码大小而不是速度),那就太好了。 (有关x86内存带宽单线程与所有内核,避免RFO的NT存储以及使用避免RFO的高速缓存协议(protocol)的 rep movs的信息,也请参阅 Enhanced REP MOVSB for memcpy,以获取更多信息)。

在较旧的CPU上, rep movsb不适用于大型副本,因此推荐的策略是 rep movsdmovsq,对最后几个计数进行特殊处理。 (假设您将要完全使用 rep movs,例如在无法触摸SIMD vector 寄存器的内核代码中使用。)

对于在L1d或L2高速缓存中很热的中型副本,使用整数寄存器的 -mno-sse自动矢量化比 rep movs差很多,因此gcc在检查重叠后一定要使用 rep movsbrep movsq,而不是qword复制循环,除非期望输入少(例如64个字节)是常见的。

字节循环的唯一优点是代码量小;它几乎是桶的底部;对于小而未知的副本大小,像glibc这样的智能策略会更好。但这太多了以内联的代码,并且函数调用的确要付出一些代价(堆满调用密集寄存器和破坏红色区域,以及 call / ret指令和动态链接间接的实际成本)。

尤其是在不经常运行的“冷”功能中(因此,您不想在其上花费很多代码大小,增加程序的I缓存占用量,TLB局部性,要从磁盘加载的页面等) 。如果手工编写asm,通常会更了解预期的大小分布,并且能够内联快速路径并回退到其他路径。

请记住,编译器将在一个程序中可能的许多循环上做出决定,并且大多数程序中的大多数代码都在热循环之外。它不应该全部them肿。 这就是为什么除非启用了配置文件引导的优化,否则gcc默认为 -fno-unroll-loops的原因。 (不过,自动矢量化已在 -O3上启用,并且可以为诸如此类的一些小循环创建大量代码。gcc在循环序言/结尾中花费大量代码大小,但是在实际循环;尽管如此,它仍然知道每次运行外部代码时,循环将运行数百万次迭代。)

不幸的是,这并不像gcc的自动矢量化代码非常有效或紧凑。在16字节SSE情况下,它在循环清理代码上花费了大量代码大小(完全展开15字节副本)。使用32字节的AVX vector ,我们得到了一个汇总的字节循环来处理剩余的元素。 (对于一个17字节的副本,与1个XMM vector + 1字节或glibc样式重叠16字节的副本相比,这是非常糟糕的)。使用gcc7及更早版本时,它会像循环序言一样执行完全的展开操作,直到对齐边界为止,因此膨胀了两倍。

IDK(如果配置文件引导的优化会在此处优化gcc的策略),例如当每次调用的计数都较小时,倾向于使用较小/较简单的代码,因此将无法实现自动向量化的代码。如果代码是“冷的”并且每次运行整个程序仅运行一次或完全不运行,则可以更改策略。或者,如果计数通常为16或24左右,那么最后 n % 32字节的标量会很糟糕,因此理想情况下,PGO会将其变为特殊情况下较小的计数。 (但我不太乐观。)

我可能会为此报告一个GCC缺少优化的错误,它涉及在重叠检查之后检测memcpy,而不是将其纯粹留给自动矢量化器处理。和/或关于将 rep movs用作 -Os,如果有更多关于该uarch的信息可用,则可以使用 -mtune=icelake

许多软件仅使用 -O2进行编译,因此除自动矢量化程序之外的 rep movs窥孔可能会有所作为。 (但问题是这是正面的还是负面的差异)!

关于c++ - 当前的任何C++编译器都发出 “rep movsb/w/d”吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51121856/

29 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com