我正在寻找 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
指令?
clang
-mbmi
和-march=native
之间的相关区别是-mtune=znver3
。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上使用单寄存器寻址模式。)