GCC/CLANG的编译器标志,以生成(IA 32的BMI 1的)“BEXTR”指令

hsvhsicv  于 4个月前  发布在  其他
关注(0)|答案(1)|浏览(41)

我正在寻找GCC/CLANG的编译器标志来生成BEXTR指令。

template <auto uSTART, auto uLENGTH, typename Tunsigned>
constexpr Tunsigned bit_extract(Tunsigned uInput)
{
    return (uInput >> uSTART) & ~(~Tunsigned(0) << uLENGTH);
}

template unsigned bit_extract<2, 3>(unsigned);

字符串
编译器标志-std=c++17 -O2 -mbmi * 不 * 生成BEXTR指令。(我在编译器资源管理器中的示例)
编译器标志-std=c++17 -O2 -march=native * 可能 * 生成BEXTR指令。(我在编译器资源管理器中的示例)
我应该使用什么编译器标志来生成BEXTR指令?

f2uvfpb9

f2uvfpb91#

clang -mbmi-march=native之间的相关差异为**-mtune=znver3**。
Godbolt运行在AWS示例上,这些示例可以是Zen 3、Ice Lake,有时也可以是更早的Intel示例,如SKX或Cascade Lake。
bextr是AMD Zen系列上的单uop(具有1个周期延迟),因此有时值得与mov-immediate一起使用以设置控制常数。从数据输入到数据输出的关键路径延迟仅为1个周期。https://uops.info/
显然,clang知道这一点,当它对-march=znver*有用时,它会使用BEXTR(这意味着-mtune),但在-march=gracemont也是1 uop(桤木Lake P内核和一些即将推出的低功耗芯片)的情况下,它不会使用。
GCC可能根本不寻找BEXTR模式,至少不使用常量移位计数。使用您的表达式作为运行时变量start,length,它使用BMI 2 SHRX+BZHI,或者更糟糕的序列-mno-bmi2

在英特尔P核上,BEXTR为2 uops(和2c潜伏期),因此不优于即时SHR和AND。(或者GCC用于大于32的位范围的shl/shr,其中掩码不适合AND的立即数。)mov寄存器副本可以从移动消除中受益,或者更聪明的编译器可以在屏蔽之前使用rorx进行复制和移位,以优化Ice Lake,其中对于整数n,禁用了mov消除。对于32位操作数大小,rorx将比mov+shr花费更多的代码大小,但对于64位来说,这是收支平衡的,其中两个指令都需要雷克斯。

BEXTR的2个uop分别在端口0/6和端口1/5上运行。除了在桤木Lake P核上,第二个uop只能在端口1上运行。它可能与BMI 2 shrx(p0/p6)和BMI 2 bzhi的内部uop相同,其中桤木Lake进行了相同的更改1:bzhi仅为端口1,而在Haswell之后的早期Intel中为p1/5。
BMI 1是2012年AMD的Piledriver.(https://en.wikipedia.org/wiki/X86_Bit_manipulation_instruction_set)的新功能。当英特尔决定支持它时,他们决定在移位和新的bzhi操作方面实现bextr,他们直接将其公开为BMI 2指令。对于英特尔来说,BMI 1和BMI 2在Haswell中都是新功能。
AMD的TBM(trailing bit manipulation)包括bextr的立即版本,具有16位立即
BMI 1 bextr的理想用例是一个循环不变但运行时可变的位范围。打包控制字段可能比在shrx的寄存器中生成移位计数和andandn的掩码更便宜,而且只有一个寄存器专用于保存这些。
Clang针对具有运行时可变位字段位置的非循环单一用例执行此操作,但具有讽刺意味的是,不针对循环执行此操作。

// dummy arg gives it the option of mov dh, cl which would be faster on Zen 3 (no merging uop needed)
unsigned long bext_nonconst(int unused, unsigned long uInput, unsigned uSTART, unsigned uLENGTH)
{
     return (uInput >> uSTART) & ~(~0ULL << uLENGTH);
}

字符串
Godbolt编译器输出:

# clang -O3 -march=znver3
        shrx    rax, rsi, rdx
        bzhi    rax, rax, rcx
        ret
# clang -O3 -march=znver3  -mno-bmi2
        shl     ecx, 8
        movzx   eax, dl        # correctly choosing a separate destination for zero elimination
        or      eax, ecx
        bextr   rax, rsi, rax
        ret
    # could have been  mov dh, cl   to make RDX the control reg
    # but compilers only sometimes use partial registers

的数据
在一个循环中,bextr的完美用例,clang避免了它。/facepalm。GCC也是如此,但是我们刚刚看到clang使用bextr for one 提取,而不是在一个具有相同调优选项的循环中。

void bext_array(unsigned long *dst, const unsigned long *src, unsigned bitstart, unsigned bitlen){
    for(int i=0 ; i<10240 ; i++){
        dst[i] = bext_nonconst(1, src[i], bitstart, bitlen);
    }
}
# clang -O3 -march=znver3  -mno-bmi2
bext_array(unsigned long*, unsigned short*, unsigned int, unsigned int):
        mov     eax, edx    # copy bitstart for no reason
        mov     rdx, -1
        xor     r8d, r8d
        shl     rdx, cl     # -1ULL<<bitlen   - count was already in ECX
        not     rdx         # mask = ~(~0ULL << bitlen)
.LBB4_1:                                # =>This Inner Loop Header: Depth=1
        mov     r9, qword ptr [rsi + 8*r8]
        mov     ecx, eax     # could have been done outside the loop, missed optimization
        shr     r9, cl
        and     r9, rdx      # (src[i] >> bitstart) & mask
        mov     qword ptr [rdi + 8*r8], r9
        inc     r8
        cmp     r8, 10240
        jne     .LBB4_1
        ret

的字符串
循环体可能是bextr rax, [rsi + rcx*8], rdx/mov [rdi + rcx*8] + inc ecx或其他什么(显然bextr不能微融合内存源操作数,所以它总是一个额外的uop,即使在AMD和even with a one-register addressing mode on Intel上也是如此)。

Footnote 1:桤木Lake上的https://uops.info/错误延迟测量

**bextr是桤木Lake上的两个单周期uop,与早期的Intel相同。shrxbzhi仍然是单周期延迟,而不是3。**考虑到另一个得到这个正常结果的测量,以及uops.info测量中的不一致性,以及Intel CPU架构师的假定正常性,几乎可以肯定这是一个微基准测试错误。

uops.info 在shrxbzhibextr中测量了每uop有3个周期延迟的桤木Lake P内核,但从CPU设计PoV来看,这将是疯狂的(特别是shrx/shlx,而shl reg, cl对于reg结果仍然是1个周期延迟!uops.info测量了一个输入的4c延迟,而另一个输入的6c延迟,但英特尔一般不支持延迟转发(其中,UOP可以在仅准备好其一些输入的情况下开始)。如果一个UOP仅预处理不在关键路径上的输入,则3C可能是合理的,但4C不是。
他们还测量了内存源shrx从移位计数到结果的1c延迟,这与reg-source为3个周期不一致,但与1c延迟移位一致。
InstLatx64测量i9- 12900 K桤木Lake的Golden Cove P-cores时,bzhishrx具有单周期延迟,bextr具有2周期延迟,使用简单的测量序列,仅重复指令。InstLat还确认了吞吐量变化(对于bzhibextr为1/时钟)。这些都是有意义的,符合我的预期。

所以我认为桤木Lake上的指令序列uops.info用来测量延迟的东西一定很奇怪。他们使用movsxd在测试中的指令之间建立依赖链(以区分多uop指令的不同输入,以及在像shl reg,cl这样的东西的情况下,通过FLAGS基本上错误的依赖关系的延迟链比数据移位本身慢。)
uops.info 显示了每个实验的实际循环体,以及原始的perf计数器数字,这太棒了,比任何其他指令表都要好。它允许更深入地挖掘,看看在令人惊讶的情况下实际测量了什么,在以前的一些情况下,这足以看到发生了什么,并向Andreas Abel报告错误(他已经修复了以前情况下受影响指令的测试顺序。)我给他发了邮件,一旦我们弄清楚发生了什么,他会更新的。
桤木Lake seem normal上的movsxd r64, r32测量,3个端口(1/5/B)中任何一个的1c延迟,先测量MOVSXD R9, R8D,再测量movsxd r8,r9d,然后是一个4 x movsxd r8,r8d链。
(InstLatx 64报告MOVSXD r1_64, r2_32有0.3c的延迟,但实际上应该报告为“没有真正的dep”,就像cmp一样,因为他们没有创建一个将结果反馈到输入的dep链。不像他们的XCHG r1_32, r2_32,单独的数据库让我们看到1.5c的平均延迟,因为它是2c的一种方式,所有这一切告诉我们没有(假)输出依赖,就像lz/tzcntpopcnt在Intel上有一段时间了。桤木Lake还没有开始为movsx[d]做mov-elimination和为movzx做16位源代码。)
测量的数字对于他们实际测试的序列是准确的,这些是延迟测试,所以前端效应可能不是问题。例如for bextr r32,r32,r32 on ADL。对于测试“Latency operand 2 → 1:6 cycles",循环体是

0:   c4 42 28 f7 c8          bextr  r9d,r8d,r10d
   5:   4d 63 c1                movsxd r8,r9d    # feed output back to operand 2
   8:   4d 63 c0                movsxd r8,r8d    # and add some latency
   b:   4d 63 c0                movsxd r8,r8d
   e:   4d 63 c0                movsxd r8,r8d
  11:   4d 63 c0                movsxd r8,r8d


他们预计5条movsxd指令的“链延迟”为5个周期。他们测量了11.0核心时钟周期和UOPS_EXECUTED.THREAD: 7.0,这排除了movsxd的任何移动消除(这本来是令人惊讶的。)来自bextr的2个uop,加上5 movsxd。(movsxd的任何较低延迟都会使bextr的计算延迟更高,因为计算假设每个都是1c。)
我不知道movsxd有什么特别之处,但用lea r8d, [r9+1]测试也可以作为具有1个输入和一个只写目的地的1c延迟指令工作。(在早期的CPU上,莱亚在较少的端口上运行,因此movsxd是一个不错的选择。)
在桤木Lake的Golden Cove P内核上可以消除具有64位目的地和小型[reg+disp8]寻址模式的lea,零延迟,但不适用于32位目的地。(尽管如此,这可能会改变,所以lea实际上不是一个很好的未来证明的选择)。IDK他们如何写一个不同的寄存器值没有一个ALU uop;也许RAT(寄存器分配表)支持某种偏移量,可以在以后的寄存器读取过程中添加?像堆栈引擎一样,但可以由后端uops读取?它也适用于add r64, imm8,但不适用于add r32, imm8https://www.anandtech.com/show/17047/the-intel-12th-gen-core-i912900k-review-hybrid-performance-brings-hybrid-complexity/5
在Gracemont E-cores上,anandtech的文章说movsx应该有移动消除,但是uops.info测量了所有reg-source情况下的1c延迟。
LEA_B_IS_D8 (R32)例如在2c延迟下测量的lea r8d,[r14+r13*2+0x8]可能是这种效应的一个例子。Anandtech更新日志说“莱亚与规模延迟2->3时钟”为金湾与塞浦路斯湾,所以测量实际上是一个周期快于它显然应该。
其他受影响的指令包括简单整数指令的FLAG结果,如addsuband,以及andn,其中uops.info将其延迟报告为[1:2],这意味着对于某些输入和输出的组合(在本例中,输入到输出reg)为1c,对于其他组合(输入到FLAGS)为2c。因此,奇怪的不仅仅是具有单独的只写目的地的指令。(但FLAGS在技术上是一个单独的输出,而不是输入之一。)

相关问题