【问题标题】:VS: unexpected optimization behavior with _BitScanReverse64 intrinsicVS: _BitScanReverse64 内在的意外优化行为
【发布时间】:2017-05-12 02:49:44
【问题描述】:

以下代码在调试模式下工作正常,因为定义了 _BitScanReverse64 如果未设置 Bit,则返回 0。 Citing MSDN: (返回值为)“如果设置了 Index,则为非零,如果未找到设置的位,则为 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;
}

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

【问题讨论】:

  • 立即初始化索引在这里没有什么区别,因为_BitScanReverse 会进行初始化。
  • 说“程序崩溃”是指assert 失败,对吧?我已经有一段时间没有使用 VS 了,但不应该在发布版本中禁用 assert 吗?
  • 微软在 _BitScanReverse64 上的页面并没有说如果没有设置位就会设置index;它只说如果设置了一个位,index 将设置一个值。因此,如果未设置任何位,_BitScanReverse64 可能会单独留下 index,并保留它原来的任何内容。
  • @Altainia 有正确的答案。您应该检查_BitScanReverse64 的返回值。如果为零,则 index 的值未定义(至少在文档中)。假设是这样,因为只有一个非零返回表明 index 已设置。
  • index 初始化为 0,但 _BitScanReverse64 可能会在内部更改它。如果没有找到 1,则未优化的版本可能会将 index 设置为零,但优化的版本会省略该部分以节省时间。两者都符合文档(“如果设置了索引,则为非零,如果未找到设置的位,则为 0。”)并且优化版本执行得更少,因此速度更快。

标签: c++ visual-studio optimization x86-64 intrinsics


【解决方案1】:

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

尽管index arg 被引用,编译器仍将其视为仅输出。


unsigned char _BitScanReverse64 (unsigned __int32* index, unsigned __int64 mask)
Intel's intrinsics guide documentation for the same intrinsic 似乎比您链接的 Microsoft docs 更清晰,并阐明了 MS 文档试图说的内容。但仔细阅读后,它们似乎都在说同样的话,并描述了 bsr 指令的薄包装。

Intel documents the BSR instruction 在输入为 0 时产生“未定义值”,但在这种情况下设置 ZF。 但 AMD 将其记录为保持目标不变:

AMD's BSF entryAMD64 架构中 程序员手册 第 3 卷: 通用和 系统说明

... 如果第二个操作数包含 0,则指令设置 ZF 为 1 并且不改变目标寄存器的内容。 ...

在当前的 Intel 硬件上,实际行为与 AMD 的文档相匹配:当 src 操作数为 0 时,它不会修改目标寄存器。也许这就是为什么 MS 将其描述为仅在输入非零时设置 Index(并且内在函数的返回值非零)。

在 Intel (but maybe not AMD) 上,这甚至不会将 64 位寄存器截断为 32 位。例如mov rax,-1 ; bsf eax, ecx(ECX 归零)保留 RAX=-1(64 位),而不是您从 xor eax, 0 获得的 0x00000000ffffffff。但是对于非零 ECX,bsf eax, ecx 具有将零扩展到 RAX 的通常效果,例如 RAX=3。


IDK 为什么英特尔还没有记录它。也许一个非常旧的 x86 CPU(比如原始的 386?)以不同的方式实现它? Intel 和 AMD 经常go above and beyond what's documented in the x86 manuals in order to not break existing widely-used code (e.g. Windows),这可能就是这件事的开始。

在这一点上,英特尔似乎不太可能放弃该输出依赖性并留下实际垃圾或 -1 或 32 以表示 input=0,但缺乏文档使该选项保持打开状态。

Skylake 删除了 lzcnttzcnt 的错误依赖项(后来的 uarch 删除了 popcnt 的错误 dep),同时仍然保留了 bsr/bsf 的依赖项。 (Why does breaking the "output dependency" of LZCNT matter?)


当然,由于 MSVC 优化了你的 index = 0 初始化,大概它只是使用它想要的任何目标寄存器,不一定是保存 C 变量先前值的寄存器。 所以即使你想,我不认为你可以利用 dst-unmodified 行为,即使它在 AMD 上得到保证。

所以在 C++ 术语中,内在函数对 index 没有输入依赖性。但是在 asm 中,指令 确实 对 dst 寄存器有输入依赖性,就像 add dst, src 指令一样。如果编译器不小心,这可能会导致意外的性能问题。

不幸的是,在英特尔硬件popcnt / lzcnt / tzcnt asm instructions also have a false dependency on their destination 上,即使结果从不依赖它。不过,现在编译器会解决这个问题,因此在使用内部函数时您不必担心它(除非您的编译器已经使用了几年,因为它是最近才被发现的)。


您需要检查它以确保index 有效,除非您知道输入非零。例如

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)
}

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

【讨论】:

  • 人们说它是半记录的原因是因为 AMD 记录了它。见here, p. 112(PDF 中的第 148 页)。 (我找不到 AMD 的 x86-32 手册,但我发誓它说的类似。)
  • 除此之外,您关于内在函数在 MSVC 中如何工作的推测是完全正确的。我花了很多时间来玩它discovered a few bugs (only for 32-bit builds),并试图找出利用“半记录”行为的方法。不幸的是,除了内联汇编之外别无他法。此外,您应该注意,如果您不检查内在函数的返回值,则可能会生成次优代码!
  • 不幸的是,当您使用相应的内在函数时,即使是最新版本的 MSVC(VS 2015 Update 3)也无法解决 popcnt/lzcnt/tzcnt 中的错误依赖关系。尚未为此提交错误/功能请求。他们忽略了我迄今为止提交的所有错误。
  • @CodyGray:gcc 可以解决 popcnt/lzcnt/tzcnt 的错误 dep,但不适用于 bsf/bsr。 /掌心。 :( 我正在为此输入错误报告。感谢您提供的链接,尤其是 AMD 的文档。
猜你喜欢
  • 1970-01-01
  • 2018-03-11
  • 1970-01-01
  • 2018-07-08
  • 1970-01-01
  • 2021-08-28
  • 2023-04-04
  • 2020-05-01
  • 1970-01-01
相关资源
最近更新 更多