代码之家  ›  专栏  ›  技术社区  ›  geza

当前的C++编译器有没有发出“ReVMOSB/W/D”?

  •  2
  • geza  · 技术社区  · 7 年前

    这个 question 让我想知道,当前的现代编译器是否会发出 REP MOVSB/W/D 说明。

    基于此 discussion ,似乎使用 代表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 )。有没有编译器使用这个指令?

    1 回复  |  直到 7 年前
        1
  •  3
  •   Peter Cordes    7 年前

    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只会 代表MOVSB 通过识别 memcpy 模式,然后选择内联memcpy作为 代表MOVSB . 它不会直接从字节复制循环转到 代表MOVSB 这就是为什么可能的别名会破坏优化。(可能有意思的是 -操作系统 考虑使用 rep movs 但是,当别名分析无法证明它是memcpy或memmove时,直接使用 代表MOVSB (第页)

    void fn(char *__restrict dst, const char *__restrict src, int l) {
        for (int i=0; i<l; i++) {
            dst[i] = src[i];
        }
    }
    

    这可能不应该“算数”,因为我可能 对于除“使编译器使用”之外的任何用例,建议使用这些优化选项 代表MOV “,所以它与内在的没有什么不同。 我没有检查所有的 -mtune=silvermont / -mtune=skylake / -mtune=bdver2 (推土机版本2=piledriver)等调优选项,但我怀疑它们中的任何一个都能启用。所以这是一个不切实际的测试,因为没有人使用 -march=native 会得到这个代码。

    但是上面的c编译 with gcc8.1 -xc -O3 -Wall -mstringop-strategy=rep_byte -minline-all-stringops 在Godbolt编译器资源管理器上,此ASM for 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调用约定正在为内联优化 代表MOV 不是巧合( Why does Windows64 use a different calling convention from all other OSes on x86-64? )。我认为GCC倾向于在设计调用约定时这样做,因此它节省了指令。

    rep_8byte 做一个 设置处理的计数不是8的倍数,可能是对齐,我没有仔细看。

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


    内衬 代表MOVSB 如果没有对齐保证,这将是一个糟糕的选择,因此编译器在默认情况下不这样做是好事。(只要他们这样做 某物 更好。) Intel's optimization manual 有一个关于memcpy和memset的部分,其中包含simd向量vs。 代表MOV .另请参见 http://agner.org/optimize/ 和中的其他性能链接 the x86 tag wiki .

    (我怀疑GCC会采取任何不同的行动,如果你这样做的话 dst=__builtin_assume_aligned(dst, 64); 或者其他与编译器通信的方式。例如 alignas(64) 在一些阵列上。)

    英特尔的Icelake微体系结构将具有“短代表”功能,这可能会降低 代表MOV / rep stos 使它们对小的计数更有用。(目前 rep 字符串微码的启动开销很大: What setup does REP do? )


    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寄存器或 代表MOVSB (在检查了一个内部配置变量之后,该变量是在glibc初始化自身时根据CPU检测设置的)。我不确定它将实际使用哪个CPU 代表MOVSB 如果有的话,但是支持将其用于大型副本。


    代表MOVSB 对于较小的代码大小和这样的字节循环计数的不可怕的缩放,这可能是一个非常合理的选择。 安全处理不太可能出现的重叠情况。

    微码启动开销对于将它用于当前CPU上通常较小的拷贝是一个很大的问题。

    如果当前CPU上的平均拷贝大小可能是8到16个字节,并且/或者不同的计数会导致分支预测失误,那么这可能比字节循环要好。不是这样的 好的 但不那么糟糕。

    字节循环转换为字节循环的最后一道窥视孔优化 代表MOVSB 如果在没有自动矢量化的情况下编译,这可能是个好主意。 (或者对于像MSVC这样的编译器,即使在完全优化时也会进行字节循环。)

    如果编译器更直接地了解它,并考虑将其用于 -操作系统 (针对超过速度的代码大小进行优化)在使用增强的rep movs/stos byte(ermsb)功能对CPU进行优化时。(另请参见 Enhanced REP MOVSB for memcpy 对于许多关于x86内存带宽的好东西,单线程与所有核心相比,NT存储避免RFO,以及 代表MOV 使用RFO避免缓存协议…)。

    在旧的CPU上, 代表MOVSB 对于大拷贝来说不是很好,所以推荐的策略是 rep movsd movsq 对最后几个计数进行特殊处理。(假设你要使用 代表MOV 例如,在内核代码中,您不能触摸SIMD矢量寄存器。)

    这个 -mno-sse公司 使用整数寄存器的自动矢量化比 代表MOV 对于在l1d或l2缓存中处于热状态的中等大小的副本,因此gcc绝对应该使用 代表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,而不是将其完全留给自动矢量器。和/或关于使用 代表MOV 对于 -操作系统 ,可能与 -mtune=icelake 如果有更多关于那个UARCH的信息。

    很多软件都是用 -氧气 一个窥视孔 代表MOV 除了自动矢量器,其他的都会有不同。(但问题是这是一个正负差异)!