【问题标题】:repz ret: why all the hassle?repz ret:为什么这么麻烦?
【发布时间】:2017-02-13 06:23:28
【问题描述】:

repz ret 的问题已在此处 [1] 以及其他来源 [2, 3] 非常令人满意地进行了介绍。然而,没有阅读这些资料,我找到了以下问题的答案:

  1. 在与retnop; ret 的定量比较中,实际惩罚是多少?尤其是在后一种情况下——当大多数函数要么有 100 多个或者被内联时,解码一个额外的指令(以及一个空的指令!)真的相关吗?

  2. 为什么这个问题在 AMD K8 中从未得到修复,甚至进入了 K10?既然什么时候基于一个保持未记录的行为记录一个丑陋的解决方法,而不是实际解决问题,当原因的每个细节都知道的时候?

【问题讨论】:

  • 感谢匿名投票,它确实有助于澄清这个问题。
  • 这显然有助于防止分支错误预测,这是一个相当大的惩罚,但实际惩罚会因情况而异。我不确定您为什么将解决方法称为麻烦或丑陋的解决方法,因为解决方法实现起来再简单不过了,也不难理解。另一方面,在硬件中解决问题意味着完全重新设计分支预测器。这不一定是整体改进,除非增加用于实现它的宝贵裸片空间量。
  • @RossRidge 这很丑,因为它没有反映rep 前缀的描述或目的。正如我在另一个问题及其来源中所读到的那样,它只允许字符串指令,而使用 ret 一个 UB。该定义从未更新以反映(并因此正式证明)已成为普遍做法的内容。 在主要供应商处具有已知行为的 UB 仍然是 UB。 此外,因为它不会以任何方式考虑 ecx,尽管人们可能期望它的行为至少在 = 0 时有所不同vs. ≠ 0。不可否认,nop 在所有这些方面都会更干净。
  • 嗯,不,这里不是在谈论符合某些官方标准。所有与 x86 兼容的 CPU 都会忽略非字符串指令上的 0xF3 (REP) 前缀,因为原始 8086 就是这样做的。任何不这样做的 CPU 都不兼容 x86。这是英特尔在创建 PAUSE 指令(实际上是 REP NOP)以及后来创建 XACQUIRE 和 XRELEASE 前缀(实际上分别是 REP 和 REPNE 前缀)时利用的东西。这些都被记录为向后兼容,因为它们只是提示,而较旧的 CPU 会忽略“提示”。
  • 不幸的是,英特尔和 AMD 对澄清这方面的任何事情都没有太大兴趣。 x86 兼容 CPU 必须实现的所有未记录行为都会给任何其他潜在竞争对手造成负担。如果 CPU 没有忽略 RET 指令前面的 REP 前缀,Windows 可能无法启动,因为它在 __security_check_cookie 中使用,所以这是一个竞争对手必须正确处理的细节示例。

标签: assembly x86 micro-optimization amd-processor branch-prediction


【解决方案1】:

分支错误预测
所有喧嚣的原因是分支错误预测的成本。
当分支出现时,CPU 会预测所采用的分支并将这些指令预加载到管道中。
如果预测错误,则需要清除管道并加载新指令。
这可能需要多达number_of_stages_in_pipeline 个周期加上从缓存加载数据所需的任何周期。每次错误预测通常需要 14 到 25 个周期。

原因:处理器设计
K8 和 K10 之所以会出现这种情况,是因为 AMD 进行了一次漂亮的优化。
AMD K8 和 K10 将预解码缓存中的指令并跟踪它们在 CPU L1 指令缓存中的长度。
为了做到这一点,它有额外的位。

For every 128 bits (16 bytes) of instructions there are 76 bits of additional data stored

下表详细说明了这一点:

Data             Size       Notes
-------------------------------------------------------------------------
Instructions     128 bits   The data as read from memory
Parity bits      8 bits     One parity bit for every 16 bits
Pre-decode       56 bits    3 bits per byte (start, end, function) 
                            + 4 bit per 16 byte line
Branch selectors 16 bits    2 bits for each 2 bytes of instruction code

Total            204 bits   128 instructions, 76 metadata

由于所有这些数据都存储在 L1 指令缓存中,K8/10 cpu 在解码和分支预测上的工作量要少得多。这样可以节省硅。
而且由于 AMD 没有像英特尔那样大的晶体管预算,它需要更智能地工作。

但是,如果代码是 esp。紧一个跳转和一个 ret 可能占用相同的两个字节槽,这意味着 RET 被预测为未使用(因为它后面的跳转是)。
通过使 RET 占用两个字节 REP RET 这永远不会发生,并且 RET 将始终被预测为 OK。

英特尔没有这个问题,但(过去)遭受预测插槽数量有限的问题,而 AMD 则没有。

nop ret
从来没有理由做nop ret。这是两条指令浪费了一个额外的周期来执行 nopret 可能仍然与跳转“配对”。
如果要对齐,请改用REP MOV 或使用multibyte nop

结束语
只有本地分支预测与指令一起存储在缓存中。
还有一个单独的全局分支预测表。

【讨论】:

  • 我认为 gcc 使用 rep ret 如果(且仅当)RET 可以作为分支后的下一条指令运行。 (这包括JNE/RET什么的情况,也包括RET旁边没有跳转的情况,它只是一个分支target。)
  • 这正是我想看到的,数字。谢谢!只是一个问题:nop ret 如何与跳跃配对?我的意思是,按照 GCC 的逻辑,后续 跳转不会有问题,对吧?我希望它的性能或多或少与rep ret 一样好,前提是解码器知道nop 中没有太多要“执行”的东西。我不明白为什么这会解码为不仅仅是零微操作。
  • @TheVee,nop 仍然占用虚拟前缀不占用的资源。它不能解码为零微指令,因为它仍然必须移动指令指针并且它仍然必须退出。前缀没有这些问题。如果您跳入 RET,那么它仍然可以与跳跃“配对”。如果您跳入前面的 nop 则不是,而是在浪费一个循环。
  • @Johan 太棒了,真的很清楚!非常感谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-19
  • 1970-01-01
  • 1970-01-01
  • 2015-09-08
  • 1970-01-01
相关资源
最近更新 更多