GCC/CLANG 的编译器标志生成“BEXTR”指令(IA32 的 BMI1)

问题描述 投票:0回答:1

我正在寻找 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
may 生成 BEXTR 指令。 (我在编译器资源管理器中的示例

我应该使用什么编译器标志来生成

BEXTR
指令?

gcc x86 clang compiler-optimization bmi
1个回答
0
投票

clang

-mbmi
-march=native
之间的相关区别是
-mtune=znver3

Godbolt 运行在 AWS 实例上,这些实例可以是 Zen 3、Ice Lake,有时也可以是早期的 Intel,如 SKX 或 Cascade Lake。

bextr
是 AMD Zen 系列上的单微指令(具有 1 个周期延迟),因此有时值得与 mov-immediate 一起使用来设置控制常量。从数据输入到数据输出的关键路径延迟仅为1个周期。 https://uops.info/

显然 clang 知道这一点,并且当它对

-march=znver*
(这意味着 -mtune)有用时会使用 BEXTR,但对于
-march=gracemont
则不然,它也是 1 uop(Alder Lake P 核,以及一些即将推出的低功耗芯片。)是一个调整错误,如果尚未在工作中,您可以向 https://github.com/llvm/llvm-project/issues/ 报告。

GCC 可能根本不寻找 BEXTR 模式,至少不寻找恒定的班次计数。使用运行时变量开始、长度的表达式,它使用 BMI2 SHRX+BZHI,或者使用

-mno-bmi2
的更糟糕的序列。

在 Intel P 核上,BEXTR 为 2 uops(以及 2c 延迟,或者在 Alder Lake 上更糟),因此并不比直接 SHR 和 AND 更好。 (或者 GCC 用于大于 32 的位范围的

shl
/
shr
,其中掩码不适合 AND 的立即数。)
mov
寄存器副本可以受益于 mov-elimination 或更智能的方法编译器可以在屏蔽之前使用
rorx
进行复制和移位,以优化 Ice Lake,其中对整数寄存器禁用了 mov-elimination。
rorx
对于 32 位操作数大小,会比 mov+shr 花费更多的代码大小,但对于 64 位来说,这两个指令都需要 REX,因此可以达到收支平衡。

BEXTR 的 2 个 uops 一个运行在端口 0/6,另一个运行在端口 1/5。除了在 Alder Lake P 核上,第二个 uop 只能在端口 1 上运行。它可能与 BMI2

shrx
和 BMI2
bzhi
相同的内部 uop,其中 Alder Lake 进行了相同的更改:
shrx
是 3c( !) p06 的延迟 uop,
bzhi
是仅适用于 p1 的 3c 延迟 uop。两者都比早期英特尔的 1c 延迟有所提高。

(也许https://uops.info/测量发生了一些奇怪的事情?他们使用

movsxd
在被测试的指令之间建立依赖链,所以如果有什么奇怪的事情或者转发到/从它转发并转移/bzhi uops,这可以解释额外的延迟吗?但是
shrx
吞吐量测量也显示只有 1c 吞吐量,即使它在两个端口中的任意一个上运行,所以这非常奇怪。Alder Lake 的
shr reg, cl
是 2 uops,从 3 uops 下降,实际的移位部分(不涉及标志)仍然有 1c 延迟,所以我不知道为什么
shrx
不能作为那种 uop 运行。或者如果是这样,测量有什么问题。他们测量内存 -源
shrx
,从移位计数到结果有 1c 延迟。)

BMI1

bextr
的理想用例是循环不变但运行时可变的位范围。打包控制字段可能比在寄存器中生成
shrx
的移位计数和
and
andn
的掩码更便宜,而且它只是一个专用于保存该值的寄存器。

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 避免了它。 /捂脸。 GCC 也是如此,但我们只是看到 clang 使用
bextr
来进行 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
无法微融合内存源操作数,因此它始终是一个额外的微指令,即使在AMD上也是如此,并且即使在Intel上使用单寄存器寻址模式。)

© www.soinside.com 2019 - 2024. All rights reserved.