为什么 x86 只有一种形式的条件移动,而不是立即数或 8 位?

Why does x86 only have 1 form of conditional move, not immediate or 8-bit?

我注意到 Conditional Move 指令的可扩展性低于普通 mov。比如不支持立即数,不支持寄存器的低字节。

出于好奇,为什么 Cmov 命令比一般的 mov 命令限制更多?例如,为什么不允许这样的事情:

mov    , %rbx    # allowed
cmovcc , %rbx    # I suppose setcc %bl could be used for the '1' immediate case

附带说明一下,我注意到在使用 Compiler Explorer 时 cmovcc 的使用比 jmpccsetcc 少得多。通常是这种情况吗?如果是,为什么它的使用频率低于其他条件?

作为条件,它已经需要 16 个不同的操作码,仅用于 cmov r, r/m 形式,每个不同的 cc 条件一个,就像 jccsetcc (同义词当然是共享操作码)。

因此,即使还有 16 个 0F xx 操作码的“空间”,当英特尔为 Pentium Pro 添加它时,花费所有编码 space 可能也不值得。好吧,也许是 sign-extended-imm8 表格。这会占用其他新操作码的空间,例如英特尔可能已经开始设计的 MMX 和 SSE 指令,或者至少在 P6 的 ISA 扩展完成时考虑用于 Pentium-MMX 和 Pentium III。

imm8 表格在大多数情况下当您完全想要 cmov 时很有用(通常有条件地将某些内容归零),但它不是 必需的 。 RISC 哲学(英特尔通过 P61 倾向于只提供一种方式,并让代码使用 mov-immediate 在需要时在另一个寄存器中创建常量。

Out-of-order exec 通常可以隐藏 mov 的成本——立即将常量放入另一个寄存器。这样的指令独立于其他所有指令,并且可以在其调度的执行端口上有空闲周期时立即执行。 (然而,front-end 通常是一个真正的瓶颈,静态 code-size 确实很重要,所以不幸的是它不是免费的。)

脚注 1:RISC 思想对于 P6 微体系结构来说是一件大事,最著名的是将 x86 指令解码为 1 或更多微指令的革命性思想 RISC-like back-end,允许 out-of-order 执行一条 memory-destination 指令的不同部分(加载/ALU/存储),例如。

但在较小的决策中也是如此,例如 P6 不支持跨一条指令的微指令保持 TLB 一致性的硬件支持。这就是为什么 adc %reg, (mem) 需要比您在 Intel CPUs 上预期更多的微指令。 Andy Glew(在 P6 上工作的英特尔架构师)在 Stack Overflow 评论(我在 this answer 中引用)中解释说,包括说 'I was a RISC proponent when I joined P6, and my attitude was "let SW (microcode) do it".'

很容易看出这种态度如何扩展到 x86 ISA 设计,并且只提供 cmov 的最低限度形式。 (几乎不需要 8 位;你总是可以移动整个寄存器,而且你 无论如何,因为可能出现停顿。这在 PPro 上比在后来的 P6(如 Core 2)上更昂贵。Sandybridge-family partial-register合并更便宜。)

但这纯粹是我对哪些因素可能影响该设计决策的推测。


添加晶体管以解码 imm8imm32、and/or [=21] 的成本(功率和芯片面积,以及可实现的时钟速度) =] cmov 的编码必须权衡预期的 real-world 代码能够使用它的加速。 以及使用更多操作码编码的未来成本 space .

除了 coding-space 的未来成本(它让 MMX 和 SSE1 指令只有 2 字节的操作码),英特尔可能已经猜错了,省略了 cmov $sign_extended_imm8, %reg 这实际上是有用的相当频繁。


它使用较少,因为它仅在计算条件两边的结果和 select 一个条件的结果便宜时才有用,而不是仅仅分支并且只做一个。它作为一种优化很有用,尤其是当编译器期望分支预测不佳时。 Purpose of cmove instruction in x86 assembly?

关于控制依赖(分支)与数据依赖(cmov)的更一般的cpu-architecture背景:difference between conditional instructions (cmov) and jump instructions

参见 Conditional move (cmov) in GCC compiler 回复:当 GCC if-conversion 进入无分支汇编时。

使用 cmov 如果你做错了甚至会造成伤害 (),对于分支预测本可以非常准确地预测的情况(例如在排序输入数据的特殊情况下)。

在具有更短/更窄管道和更小 out-of-order 执行资源的旧 CPU 上(因此错误预测的成本更低),CMOV 在更少的情况下有用。特别是在 Broadwell 之前的 Intel 上,它需要 2 微指令而不是 1。Linux Torvalds 解释了为什么它在很多常见情况下都很糟糕,早在 2007 年就对 Core 2 CPU 进行了一些测试:https://yarchive.net/comp/linux/cmov.html

当然,编译器生成它的情况并不少见,但是,如果您根据条件编写 select 来自几个值的代码。 Clang 的启发式方法倾向于使用比 GCC 更多的 cmov,即更积极 if-conversion 到无分支。


请注意,setcc 也不会经常使用,除非您经常查看 non-inlined 版本的 return 布尔值的函数。

我在我的 Arch Linux 桌面上反汇编了 libperl.so(只是选择了一个随机的大型二进制文件),由 GCC 10.1.0 编译。在总共 377835 条指令中 (objdump -d | egrep ' +[0-9a-f]+:'| wc -l):

  • setcc出现了1783次,经常在setxx a/setxx b/or a,b多条件下做一个分支。

  • cmovcc出现了1737次。 objdump -drwC -Mintel /usr/lib/perl5/5.32/core_perl/CORE/libperl.so | egrep 'cmov[a-z]+ ' | wc