我有一个位的位置(它永远不会为零),通过使用tzcnt计算,我想从该位置开始将高位置零。这是用C++和反汇编编写的代码(我使用的是MSVC):
auto position = _tzcnt_u64(xxx);
auto masked =_bzhi_u64(yyy, static_cast<uint32_t>(position));
字符串
tzcnt rcx,rdx
mov ecx,ecx
bzhi rax,rbx,rcx
型
BZHI接受unsigned int作为第二个参数,但只使用rcx中的位[7..0],因此我认为这个'mov'指令是不必要的。
我使用它来计算popcount,所以我也可以使用<<(64-position)。
问题是-这两个代码的执行时间相同,尽管bzhi的执行速度应该比sub+shlx快,所以mov可能会造成差异。
有没有办法避免它,或者这是编译器的事情?
2条答案
按热度按时间yeotifhr1#
这是MSVC遗漏的最佳化作业。GCC/clang可以使用
bzhi
直接在tzcnt
的输出上为你的源代码。所有的编译器都在某些情况下错过了优化,但GCC和clang的情况往往比MSVC少。(And GCC在针对Haswell进行调优时会小心地中断the output dependency of
tzcnt
,以避免通过错误的依赖关系创建循环传递依赖关系链的风险。不幸的是,GCC仍然对-march=skylake
这样做,它没有tzcnt
的假dep,只有popcnt
。具有讽刺意味的是,GCC并没有打破bsr/bsf
在任何CPU上的“真正”依赖性。)英特尔将
_bzhi_u64
的第二个输入记录为unsigned __int32 index
。(出于某种原因,您将static_cast
显式转换为uint32_t,但删除显式转换没有任何帮助)。IDK MSVC如何定义内部函数或在内部处理它。IDK为什么MSVC要这样做;我想知道它是否是MSVC的
_bzhi_u64
内在函数的内部逻辑中64位的零扩展,该函数采用32位C输入,但使用64位asm寄存器。(tzcnt
的输出值范围是0..64,因此在这种情况下,该零扩展是无操作的)屏蔽弹出项:移位
yyy
而不是对其进行掩码在What is the efficient way to count set bits at a position or lower?中,将不需要的位移出,而不是将它们就地归零,效率会更高。(尽管
bzhi
避免了创建掩码的成本,因此这只是盈亏平衡,但执行端口bzhi
与shlx
可以在其上运行。)popcnt
不关心位在哪里。(**FIXME:C++和asm使用右移,这会丢弃低位。我应该使用左移来移出高位。**当我写这篇文章时,我可能在考虑丢弃低位,因为
tzcnt
在另一个输入中计数低位零。左移和右移的执行方式相同,所以我暂时不做任何解释。)字符串
戈德博尔特上的MSVC
型
前端总共3个uop =当与其他周围代码混合时,对于整体吞吐量来说非常好。
后端瓶颈:英特尔CPU上的端口1(tzcnt和popcnt)为2个微操作。(shrx在端口0或端口6上运行,作为单个uop。启用AVX 2(显然是为MSVC启用BMI 2)非常重要,否则它将使用3-uop
shr rax, cl
)关键路径延迟:yyy
到结果:1个用于SHRX,3个用于popcnt = 4个周期xxx
到结果:TZCNT的3个加上上述= 7个周期不幸的是,GCC对于破坏错误的依赖关系过于谨慎,从而耗费了额外的前端带宽。(但没有额外的后端成本)
型
不含
tzcnt
的低延迟替代方案(But uop越多,前端吞吐量就可能越差。后端执行端口压力的好处取决于周围的代码。)
BMI 1有一些bithack指令来做一些事情,比如隔离最低的设置位,所有的1微操作都有英特尔上的单周期延迟。(AMD Zen将其作为2个微操作、2个周期延迟来运行:uops.info)的数据类型
blsmsk
-获取掩码,直至(包括)最低设置位。您的原始值 * 不 * 包含xxx
中的LSB,因此很遗憾,此掩码不能直接使用。的字符串
或者
blsi
将隔离最低设置位。blsi(xxx) - 1
将创建一个掩码,该掩码最多为(但 * 不 * 包括)该最低设置位。(对于xxx=1
,我们将得到型
MSVC按预期编译,与clang相同:
型
GCC使用2的补码单位将其转换为this,使用可以在任何端口上运行的较短指令。(
andn
只能在Haswell / Skylake上的端口1或端口5上运行)型
这是3个微操作(不包括popcnt),但从
xxx
-> result到tzcnt
/shrx
只有3个周期的延迟,而tzcnt
/shrx
只有4个周期。(所有这些都不包括3个周期的popcnt延迟)更重要的是,它不与popcnt
竞争端口1。(The MSVC将其编译为
blsi
+dec
+andn
方式,对于端口1 /端口5是2微操作)最佳选择将取决于周围的代码,无论是吞吐量还是延迟是瓶颈。
如果您要对连续存储的许多不同掩码执行此操作,SIMD可能会很有效。避免使用
tzcnt
意味着您可以使用需要几条指令的双栈来执行最低设置的隔离或掩码。例如,blsi
是(-SRC) bitwiseAND (SRC)
,如英特尔asm手册的“操作”部分所述。(查找位图表达式的方便位置。)blsmsk
为(SRC-1) XOR (SRC)
SIMD popcnt可以使用
vpshufb
在每个字节的两个半字节上执行4位并行LUT,并且可以使用vpsadbw
水平累加每个元素的计数。(用于模拟Ice Lake的AVX 512vpopcntq
)anhgbhbe2#
它是一个编译器的东西(从Visual C++ 2019 00435-60000-00000-AA 388开始)。
MSVC的immintrin. h定义了
字符串
遵循英特尔与command documentation相矛盾的次优内在定义(所有
bzhi
参数大小相同)。clang has in bmi2intrin. h
型
因此不需要在代码中修改
_tzcnt_u64
结果。我修补了MSVC的immintrin. h-无济于事。可悲!因为Peter复杂的解决方法不适用于我的情况(lzcnt/bzhi,no popcnt)。