assembly 避免bzhi(y,tzcnt(x))中不必要的mov ecx,ecx指令

xfyts7mz  于 2023-08-06  发布在  其他
关注(0)|答案(2)|浏览(125)

我有一个位的位置(它永远不会为零),通过使用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可能会造成差异。
有没有办法避免它,或者这是编译器的事情?

yeotifhr

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避免了创建掩码的成本,因此这只是盈亏平衡,但执行端口bzhishlx可以在其上运行。)popcnt不关心位在哪里。
(**FIXME:C++和asm使用右移,这会丢弃低位。我应该使用左移来移出高位。**当我写这篇文章时,我可能在考虑丢弃低位,因为tzcnt在另一个输入中计数低位零。左移和右移的执行方式相同,所以我暂时不做任何解释。)

uint64_t popcnt_shift(uint64_t xxx, uint64_t yyy) {
    auto position = _tzcnt_u64(xxx); 
    auto shifted = yyy >> position;
    return _mm_popcnt_u64(shifted);
}

字符串

戈德博尔特上的MSVC

;; MSVC 19.24 -O2 -arch:AVX2  (to enable BMI for andn)
;; also clang10.0 -O3 -march=haswell  makes this asm
unsigned __int64 popcnt_shift(unsigned __int64,unsigned __int64) PROC
        tzcnt   rax, rcx
        shrx    rax, rdx, rax
        popcnt  rax, rax
        ret     0


前端总共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对于破坏错误的依赖关系过于谨慎,从而耗费了额外的前端带宽。(但没有额外的后端成本)

# GCC10.1
        xor     eax, eax          # could have just done tzcnt rdi,rdi
        tzcnt   rax, rdi
        shrx    rsi, rsi, rax
        xor     eax, eax          # pointless: RAX was already part of the dep chain leading to this.
        popcnt  rax, rsi          # GCC7.5 shifts into RAX for popcnt rax,rax to avoid this dep-breaking xor.
        ret

不含tzcnt的低延迟替代方案

(But uop越多,前端吞吐量就可能越差。后端执行端口压力的好处取决于周围的代码。)
BMI 1有一些bithack指令来做一些事情,比如隔离最低的设置位,所有的1微操作都有英特尔上的单周期延迟。(AMD Zen将其作为2个微操作、2个周期延迟来运行:uops.info)的数据类型
blsmsk-获取掩码,直至(包括)最低设置位。您的原始值 * 不 * 包含xxx中的LSB,因此很遗憾,此掩码不能直接使用。

uint64_t zmask_blsmsk(uint64_t xxx, uint64_t yyy) {
    auto mask = _blsmsk_u64(xxx); 
    auto masked = yyy & ~(mask<<1);
    return masked;
}
;; 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

的字符串

或者blsi将隔离最低设置位。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的补码单位将其转换为this,使用可以在任何端口上运行的较短指令。(andn只能在Haswell / Skylake上的端口1或端口5上运行)

;; GCC7.5 -O3 -march=haswell.   Later GCC wastes a `mov` instruction
        blsi    rax, rdi
        neg     rax
        and     rax, rsi

这是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 512 vpopcntq

anhgbhbe

anhgbhbe2#

它是一个编译器的东西(从Visual C++ 2019 00435-60000-00000-AA 388开始)。
MSVC的immintrin. h定义了

__int64 _bzhi_u64(unsigned __int64, unsigned int);

字符串
遵循英特尔与command documentation相矛盾的次优内在定义(所有bzhi参数大小相同)。
clang has in bmi2intrin. h

unsigned long long _bzhi_u64(unsigned long long __X, unsigned long long __Y)


因此不需要在代码中修改_tzcnt_u64结果。
我修补了MSVC的immintrin. h-无济于事。可悲!因为Peter复杂的解决方法不适用于我的情况(lzcnt/bzhi,no popcnt)。

相关问题