【问题标题】:Confusion about bsr and lzcnt关于 bsr 和 lzcnt 的困惑
【发布时间】:2014-10-30 06:40:27
【问题描述】:

我对这两个指令都有点困惑。首先让我们丢弃扫描值为 0 且 undefined/bsr 或 bitsize/lzcnt 结果的特殊情况 - 这种差异很明显,不是我的问题的一部分。

我们取二进制值0001 1111 1111 1111 1111 1111 1111 1111

根据英特尔的规范,lzcnt 的结果是 3

根据英特尔的规范,bsr 的结果是 28

lzcnt 计数,bsr 返回到第 0 位(即 lsb)的索引或距离。

如果 CPU 上没有可用的 BMI,两条指令如何相同?如何将 lzcnt 模拟为 bsr?或者在bsr msb 的情况下是位 0?英特尔规范中的两种“代码操作”也不同,一个从左侧计数或索引,另一个从右侧计数。

也许有人可以对此有所了解,如果没有 BMI/lzcnt 指令,我没有 CPU 来测试回退到 bsr 的结果是否相同(因为值 0 的特殊情况要扫描永远不会发生)。

【问题讨论】:

    标签: assembly x86 bmi


    【解决方案1】:

    LZCNT 给出前导零位的数量。 BSR 给出最高有效 1 位的位索引。所以他们对非零情况有效地做同样的事情,除了结果的解释不同。因此,您只需从 31 中减去 BSR 结果即可获得与 LZCNT 相同的行为,即 LZCNT == (31 - BSR)

    【讨论】:

    • 现在你让我更困惑了。对不起。如果结果不同,那么它们实际上做的事情就不一样了。他们寻找相同的位,但在程序中我依赖于结果。如果 lzcnt 和 lzcnt-as-bsr-emulation-on-non-bmi-cpu 的结果不同,那么我不能使用它,必须测试 BMI。 (特殊无所谓,扫描值永远不会为0)。那么为什么 lzcnt 完全回退到 bsr 呢?
    • 没有“回退”——它们是两条不同的指令——你需要使用条件汇编,或者进行运行时检查,并可能实现某种调度程序,这样你要么使用LZCNTBSR + SUB.
    • 现在我明白了。在没有 BMI 的 CPU 上,lzcnt 指令不会 #ud 而是执行 bsr。结果与 lzcnt 不同。我在这里误解了规范。忽略扫描值为零时的特殊情况,tzcnt 和 bsf 的行为相同,但 lzcnt 和 bsr 则不同。很多网页都说两者都是。这显然是不正确的,也是我感到困惑的原因。感谢您清除它。
    • 顺便说一下(和后期),你也可以实现为LZCNT = BSR ^ 31,这样就省了一条指令
    • @harold 这实际上是 GCC 的 __builtin_clz 的实现方式。如果你执行__builtin_clz(x) ^ 31,它会产生一个BSR,因为XOR 被取消了。
    【解决方案2】:

    需要明确的是,从 lzcntbsr 没有工作后备。发生的事情是,英特尔使用了之前的冗余序列rep bsr 来编码新的lzcnt 指令。为bsr 使用多余的rep 前缀通常被定义为被忽略,但需要注意的是它在未来的CPU 上可能会以不同的方式解码1

    因此,如果您碰巧在不支持它的 CPU 上执行 lzcnt,它将作为 bsr 执行。当然,这个 fallback 并不是故意的,它给出了错误的结果(正如 Paul R 指出的那样,他们看的是同一个位,但报告的方式不同):这只是新指令被编码,之前的 CPU 如何处理无意义的 rep 前缀。所以这个世界回退对于lzcntbsr来说是完全不合适的。

    tzcntbsf 的情况更为微妙。它使用相同的编码技巧:tzcntrep bsf 具有相同的编码,但这里的“回退”大部分都有效,因为 tzcnt 返回与 bsf 相同的值,除了零。对于零输入 tzcnt 返回 32,但 bsf 未定义目标。

    你甚至不能真正使用这个后备:如果你从来没有零输入,你最好只使用bsf,节省一个字节并与几十年的 CPU 兼容,如果你确实有零输入行为不同。

    因此,这种行为可能比后备更适合归类为琐事...


    1 通常这或多或少是深奥的,但您可以例如使用 rep 前缀,因为它们没有功能性影响来延长指令以帮助对齐后续代码而无需插入显式 @987654343 @ 指示。鉴于“将来可能会以不同的方式解码”,这在编译可能在未来任何 CPU 上运行的代码时会很危险。

    【讨论】:

    • 我相信 BSF 和 BSR 通常的“未定义”行为是在源操作数为 0 时保持目标不变。在这种情况下,即使保证源操作数不为 0,它也会最好分别使用 TZCNT 或 LZCNT ^ 31 而不是 BSF 或 BSR,因为较新的指令不会依赖于源寄存器的旧值。或者至少在理论上,显然这在实践中不起作用:stackoverflow.com/questions/21390165/…
    • 事实上,实际上覆盖其第一个操作数(而不是将其用作输入)的两条操作数指令中有几条具有这种错误依赖性(另请参阅popcnt)。也许它会在未来的架构中得到修复。 AFAIK 并非所有芯片都将行为实现为“源不变” - 显然一些 AMD 芯片将其设置为零或其他东西。 @罗斯里奇
    • AMD 的 AMD64 手册 (March 2017 version) 明确记录了 dest-unchanged 行为。:如果第二个操作数包含 0,则指令将 ZF 设置为 1 并且不会更改内容目标寄存器有趣的事实:@RossRidge:Skylake 修复了tz/lzcnt 的错误dep,但没有修复popcnt。 (IIRC,对于 popcnt 有一个错误的 dep 有一个 SKL 勘误表,所以也许他们打算这样做。:P)
    • rep noppause。通常与 NOP 一起使用的填充前缀是 66operand-size。我认为英特尔说一些关于忽略不适用于指令的前缀的 CPU。他们指出,未来的 CPU 可能会以不同的方式对其进行解码,这正是这里发生的情况,但与旧 CPU 的“兼容性”是相当明确的。由于这个原因,当输入已知为非零时,一些编译器(带有-mtune=generic)将使用tzcnt 而不是bsf,因为tzcnt 在AMD 上要快得多。我怀疑从输入操作数而不是输出设置标志会伤害他们。
    • @PeterCordes - 好点。我更新了答案以表明英特尔和 AMD 显然允许冗余的 rep 前缀,但在未来保持开放以不同方式解码它们的选项 - 这使得它们对于编译为本机代码甚至可能运行在另一个上实际上是无用的CPU(但对于 JIT 或 AOT 编译器代码可能仍然有用),因为您永远无法确定您的二进制文件不会损坏。最近似乎他们对前缀行为更加严格,通常定义确切的语义和#GPing,如果不遵循......
    猜你喜欢
    • 2011-12-13
    • 2017-12-04
    • 2012-03-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-17
    • 2020-03-06
    • 2016-03-16
    相关资源
    最近更新 更多