C++ 避免BZI(y,tzcnt(x))中不必要的mov ecx、ecx指令
我有一个位位置(它永远不会为零),通过使用tzcnt计算,我想从该位置开始将高位归零。 这是C++和拆卸的代码(我使用MSVC): BZI接受无符号int作为第二个参数,但只使用来自rcx的位[7..0],因此我认为这个“mov”指令是不必要的C++ 避免BZI(y,tzcnt(x))中不必要的mov ecx、ecx指令,c++,assembly,visual-c++,bit-manipulation,compiler-optimization,C++,Assembly,Visual C++,Bit Manipulation,Compiler Optimization,我有一个位位置(它永远不会为零),通过使用tzcnt计算,我想从该位置开始将高位归零。 这是C++和拆卸的代码(我使用MSVC): BZI接受无符号int作为第二个参数,但只使用来自rcx的位[7..0],因此我认为这个“mov”指令是不必要的 我用它来计算popcount,所以我也可以用类似的东西,这是一个MSVC遗漏的优化。GCC/clang可以直接在源代码的tzcnt输出上使用bzhi。在某些情况下,所有编译器都缺少优化,但GCC和clang的优化情况往往比MSVC少 (在为Haswell
我用它来计算popcount,所以我也可以用类似的东西,这是一个MSVC遗漏的优化。GCC/clang可以直接在源代码的
tzcnt
输出上使用bzhi
。在某些情况下,所有编译器都缺少优化,但GCC和clang的优化情况往往比MSVC少
(在为Haswell进行调优时,GCC会小心地中断,以避免通过该错误依赖项创建循环携带的依赖项链的风险。不幸的是,GCC仍然使用-march=skylake
来实现这一点,它没有为tzcnt
设置错误的dep,只有popcnt
和为bsr/bsf
设置“正确”的dep)
英特尔将
\u bzhi\u u64
的第二个输入记录为无符号\uuu int32索引
。(出于某种原因,您正在通过对uint32的静态强制转换
使其显式化,但删除显式强制转换没有帮助)。IDK MSVC如何定义内在函数或在内部处理它
IDK MSVC为什么要这样做;我想知道是不是MSVC的\u bzhi\u u64
内部逻辑中的64位零扩展,它接受32位C输入,但使用64位asm寄存器。(tzcnt
的输出值范围为0..64,因此在这种情况下,此零扩展是不可操作的)
屏蔽popcnt:shift
yyy
而不是屏蔽它
如中所述,只需将不需要的位移出,而不是将它们就地归零,效率会更高。(虽然bzhi
避免了创建掩码的成本,因此这只是盈亏平衡,执行端口bzhi
与shrx
可以运行的模差。)popcnt
不关心位在哪里
uint64_t popcnt_shift(uint64_t xxx, uint64_t yyy) {
auto position = _tzcnt_u64(xxx);
auto shifted = yyy >> position;
return _mm_popcnt_u64(shifted);
}
3前端的总UOP=与其他周围代码混合时,总体吞吐量非常好
后端瓶颈:英特尔CPU上端口1(tzcnt和popcnt)有2个UOP。(shrx作为单个uop在端口0或端口6上运行。启用AVX2显然会为MSVC启用BMI2非常重要,否则它将使用3-uopshr rax,cl
)
关键路径延迟:
- 从
到结果:1表示SHRX,3表示popcnt=4个周期yyy
- 从
到结果:TZCNT为3加上上述=7个循环xxx
xxx
中的LSB,因此不幸的是,此掩码不能直接使用
uint64_t zmask_blsmsk(uint64_t xxx, uint64_t yyy) {
auto mask = _blsmsk_u64(xxx);
auto masked = yyy & ~(mask<<1);
return masked;
}
或将隔离最低设置位。该
blsi(xxx)-1
将创建一个高达但不包括它的掩码。(对于xxx=1
,我们将获得
uint64_t zmask2(uint64_t xxx, uint64_t yyy) {
auto setbit = _blsi_u64(xxx);
auto masked = yyy & ~(setbit-1); // yyy & -setbit
return masked;
}
MSVC按预期编译,与clang相同:
blsi rax, rcx
dec rax
andn rax, rax, rdx
ret 0
GCC使用2的补码标识将其转换为该标识,使用可以在任何端口上运行的较短指令。(andn
只能在Haswell/Skylake上的端口1或端口5上运行)
这是3个UOP(不包括popcnt),但从xxx
->结果来看,只有3个周期的延迟,低于tzcnt
/shrx
(所有这些都不包括3个周期的popcnt延迟),更重要的是,它不会与popcnt
竞争端口1
(不过,MSVC将其编译为blsi
+dec
+andn
的方式是端口1/端口5的2个UOP。)
最佳选择将取决于周围的代码,吞吐量或延迟是瓶颈。
如果对连续存储的多个不同掩码执行此操作,SIMD可能会很有效。避免使用tzcnt
意味着您可以使用包含两条指令的位破解来执行最低集隔离或掩码。例如blsi
是(-SRC)bitwiseAND(SRC)
,如英特尔asm手册的操作部分所述。(查找位图表达式的方便位置。)blsmsk
是(SRC-1)XOR(SRC)
SIMD POCPNT可以用
MSVC的imIntrin.h定义
__int64 _bzhi_u64(unsigned __int64, unsigned int);
遵循与之矛盾的英特尔次优参数(所有bzhi
param的大小相同)。
叮当声在bmi2intrin.h中的作用
unsigned long long _bzhi_u64(unsigned long long __X, unsigned long long __Y)
因此不需要在代码中触摸\u tzcnt\u u64
结果
我修补了MSVC的imimintrin.h-但没有用。悲哀!因为Peter复杂的解决方法不适用于我的情况(lzcnt/bzhi,没有popcnt)。你在做发布版本吗?@user1937198是的,这是在发布中。嗯,gcc和clang都不包括这个mov,所以它看起来确实是MSVC的东西:(您通过对uint32的静态转换来明确这一点…-这只是为了删除可能丢失数据的警告。Blsmsk应该可以做到这一点。我可以稍微更改代码以在位置包含位。因此,不是7个周期,而是至少在英特尔上需要5C。@Marka:Ok perfe
;; MSVC -O2 -arch:AVX2 (to enable BMI for andn)
blsmsk rax, rcx
add rax, rax ; left shift
andn rax, rax, rdx ; (~stuff) & yyy
ret 0
uint64_t zmask2(uint64_t xxx, uint64_t yyy) {
auto setbit = _blsi_u64(xxx);
auto masked = yyy & ~(setbit-1); // yyy & -setbit
return masked;
}
blsi rax, rcx
dec rax
andn rax, rax, rdx
ret 0
;; GCC7.5 -O3 -march=haswell. Later GCC wastes a `mov` instruction
blsi rax, rdi
neg rax
and rax, rsi
__int64 _bzhi_u64(unsigned __int64, unsigned int);
unsigned long long _bzhi_u64(unsigned long long __X, unsigned long long __Y)