VS:_BitScanReverse64内在的意外优化行为

Cam*_*eon 4 c++ optimization x86-64 intrinsics visual-studio

以下代码在调试模式下工作正常,因为如果没有设置Bit,_BitScanReverse64被定义为返回0.引用MSDN :(返回值为)"如果设置了索引则为非零,如果未找到设置位,则为0."

如果我在发布模式下编译此代码它仍然有效,但如果我启用编译器优化,例如\ O1或\ O2,则索引不为零且assert()失败.

#include <iostream>
#include <cassert>

using namespace std;

int main()
{
  unsigned long index = 0;
  _BitScanReverse64(&index, 0x0ull);

  cout << index << endl;

  assert(index == 0);

  return 0;
}
Run Code Online (Sandbox Code Playgroud)

这是预期的行为吗?我正在使用Visual Studio Community 2015,版本14.0.25431.01更新3.(我离开了cout,因此在优化期间不会删除变量索引).还有一个有效的解决方法或我不应该直接使用此编译器内在?

Pet*_*des 9

AFAICT,当输入为零时内在留下垃圾index,弱于asm指令的行为.这就是为什么它有一个单独的布尔返回值和整数输出操作数.


index
英特尔针对同一内在函数的内在指南文档似乎比您链接的Microsoft文档更清晰,并且阐明了MS文档试图说的内容.但仔细阅读后,他们似乎都表达了相同的观点,并描述了unsigned char _BitScanReverse64 (unsigned __int32* index, unsigned __int64 mask)指令周围的薄包装.

bsr当输入为0时,英特尔将该指令记录为产生"未定义值",但在这种情况下设置ZF. 但AMD将其记录为保持目的地不变

在当前的英特尔硬件上,实际行为与AMD的文档相符:当src操作数为0时,它会使目标寄存器保持不变.也许这就是为什么MS将其描述为仅BSR在输入为非零时设置(并且内在的返回值为非-零).

IDK为什么英特尔还没有记录它.也许一个非常古老的x86 CPU(就像原来的386?)以不同的方式实现它?英特尔和AMD经常超越x86手册中记录的内容,以免破坏现有代码(例如Windows),这可能就是这种情况的开始.在这一点上,它们似乎不太可能会丢弃输出依赖关系,并且实际上是垃圾,或者输入= 0时为-1或32,但缺少文档会使该选项保持打开状态.


当然,由于MSVC优化了您的Index初始化,可能它只使用它想要的任何目标寄存器,而不一定是保存C变量的先前值的寄存器. 所以即使你想这样做,我也不认为你可以利用dst未修改的行为,即使它在AMD上有所保证.

因此,在C++术语中,内在函数没有输入依赖性mov rax,-1.但是在asm中,指令确实对dst寄存器具有输入依赖性,就像bsf eax, ecx指令一样.如果编译器不小心,这可能会导致意外的性能问题.

不幸的是,在英特尔硬件上,0x00000000ffffffffasm指令对其目标也有错误依赖,即使结果从不依赖于它.编译器现在可以解决这个问题,因为它在使用内在函数时不必担心它(除非你有一个编译器超过几年,因为它最近才被发现).


您需要检查它以确保xor eax, 0有效,除非您知道输入非零.例如

if(_BitScanReverse64(&idx, input)) {
    // idx is valid.
    // (MS docs say "Index was set")
} else {
    // input was zero, idx holds garbage.
    // (MS docs don't say Index was even set)
    idx = -1;     // might make sense, one lower than the result for bsr(1)
}
Run Code Online (Sandbox Code Playgroud)

如果您想避免使用这个额外的检查分支,如果您的目标是足够新的硬件(例如Intel Haswell或AMD Bulldozer IIRC),您可以通过不同的内在函数使用该bsf eax, ecx指令.即使输入全为零,它也"有效",实际上计数前导零而不是返回最高设置位的索引.