据我了解,cmp 指令将在您的标志寄存器中设置一些位。然后,您可以使用jle,jnp等指令基于这些进行分支。
我想知道的是,如何从比较中恢复整数值。
示例:以下是有效的 c 语法
y = x[a >= 13];
因此,将 a 与 13 进行比较以获得 true 或 false,分别解释为 1 或 0。但是,必须将 1 或 0 作为整数馈送到数组访问中。编译器会做什么?
我能想到的一些事情是:
进行比较,然后分支到 x[0] 或 x[1]
进行比较,然后分支执行 tmp = 0 或 tmp = 1,然后执行 x[tmp]
也许在标志上做一些花哨的逻辑(不确定是否有直接访问标志的说明)
我试图看看 gcc 为这个代码示例吐出了什么,但不可能从它抛出的所有额外垃圾中挑选出逻辑。
我正在研究编译器,因此任何建议将不胜感激。
基本上有三种方法可以做到这一点。我将一次一个地介绍它们。
一种方法基本上是您在问题中描述的:进行比较,然后分支到分别实现这两种可能性的代码。例如:
cmp [a], 13 ; compare 'a' to 13, setting flags like subtraction
jge GreaterThanOrEqual ; jump if 'a' >= 13, otherwise fall through
mov eax, [x * 0 * sizeof(x[0])] ; EAX = x[0]
jmp Next ; EAX now loaded with value, so do unconditional jump
GreaterThanOrEqual:
mov eax, [x * 1 * sizeof(x[0])] ; EAX = x[1]
; EAX now loaded with value; fall through
Next:
mov [y], eax ; store value of EAX in 'y'
通常,编译器会尝试在寄存器中保留更多值,但这应该可以让您了解基本逻辑。它进行比较,要么分支到读取/加载x[1]
的指令,要么分支到读取/加载x[0]
的指令。然后,它移动到一条指令上,该指令将该值存储到y
中。
您应该能够看到,由于需要所有分支,因此效率相对较低。因此,优化编译器不会生成这样的代码,尤其是在具有基本三元表达式的简单情况下:
(a >= 13) ? 1 : 0
甚至:
(a >= 13) ? 125 : -8
有一些技巧可以用来做这个比较,并得到相应的整数,而不必做一个分支。
这就把我们带到了第二种方法,即使用SETcc
指令。cc
部分代表"条件代码",所有条件代码都与条件跳转指令的条件代码相同。(实际上,您可以将所有条件跳转指令编写为Jcc
。例如,jge
的意思是"如果大于或等于则跳跃";同样,setge
的意思是"如果大于或等于则设置"。简单。
关于SETcc
的诀窍是它设置了一个字节大小的寄存器,这基本上意味着AL
、CL
、DL
或BL
(有更多的选项;你可以设置其中一个寄存器的高字节,和/或在64位长模式下有更多的选项,但这些是操作数的基本选择)。
下面是实现此策略的代码示例:
xor edx, edx ; clear EDX
cmp [a], 13 ; compare 'a' to 13, setting flags like subtraction
setge dl ; set DL to 1 if greater-than-or-equal, or 0 otherwise
mov eax, [x * edx * sizeof(x[0])]
mov [y], eax
很酷,对吧?分支被淘汰。所需的 0 或 1 直接加载到DL
中,然后用作加载的一部分(MOV
指令)。
这里唯一有点令人困惑的是,您需要知道DL
是完整的 32 位EDX
寄存器的低字节。这就是为什么我们需要预先清除完整的EDX
,因为setge dl
只影响低字节,但我们希望全EDX
为 0 或 1。事实证明,对所有处理器进行全寄存器预归零是执行此操作的最佳方法,但还有其他方法,例如在SETcc
指令后使用MOVZX
。链接的答案对此非常详细,所以我不会在这里赘述。关键点只是SETcc
只设置寄存器的低字节,但后续指令要求整个32位寄存器都有该值,因此需要消除高字节中的垃圾。
无论如何,这是编译器在您编写类似y = x[a >= 13]
的东西时 99% 生成的代码。SETcc
指令为您提供了一种根据一个或多个标志的状态设置字节的方法,就像您可以在标志上进行分支一样。这基本上就是您正在考虑的允许直接访问标志的指令。
这将实现
(a >= 13) ? 1 : 0
但是如果你想做呢
(a >= 13) ? 125 : -8
就像我之前提到的?好吧,您仍然使用SETcc
指令,但是之后您会做一些花哨的微调,以将生成的 0 或 1 "修复"为您实际想要的值。例如:
xor edx, edx
cmp [a], 13
setge dl
dec edx
and dl, 123
add edx, 125
; do whatever with EDX
这几乎适用于任何二进制选择(两个可能的值,取决于条件),并且优化编译器足够聪明,可以解决这个问题。仍然是无分支代码;很酷。
还有第三种实现方式,但它在概念上与我们刚刚讨论的第二种方式非常相似。它使用条件移动指令,这只是基于标志状态进行无分支集的另一种方法。条件移动指令是CMOVcc
的,其中cc
再次引用"条件代码",与前面的例子完全相同。CMOVcc
指令大约在 1995 年随奔腾 Pro 一起推出,此后一直在所有处理器中(好吧,不是奔腾 MMX,而是奔腾 II 及更高版本),所以基本上是你今天看到的一切。
代码非常相似,只是顾名思义它是一个有条件的移动,因此需要进行更多的初步设置。具体来说,您需要将候选值加载到寄存器中,以便选择正确的值:
xor edx, edx ; EDX = 0
mov eax, 1 ; EAX = 1
cmp [a], 13 ; compare 'a' to 13 and set flags
cmovge edx, eax ; EDX = (a >= 13 ? EAX : EDX)
mov eax, [x * edx * sizeof(x[0])]
mov [y], eax
请注意,将EAX
移动到EDX
是有条件的 - 仅当标志指示条件ge
(大于或等于)时才会发生。因此,它适用于基本的 C 三元操作,如指令右侧的注释中所述。如果标志指示ge
,则EAX
将移动到EDX
中。否则,不会移动任何内容,EDX
将保留其原始值。
请注意,尽管某些编译器(特别是英特尔的编译器,称为 ICC)更喜欢CMOV
指令而不是SET
指令,但这与我们之前在SETGE
中看到的先前实现相比没有任何优势。事实上,它确实是次优的。
当CMOV
真正发挥作用时,它允许您消除获取旧 0 或 1 以外的值所需的位叽叽喳喳的代码。例如:
mov edx, -8 ; EDX = -8
mov eax, 125 ; EAX = 125
cmp [a], 13 ; compare 'a' to 13 and set flags
cmovge edx, eax ; EDX = (a >= 13 ? EAX : EDX)
; do whatever with EDX
现在,这是更少的指令,因为正确的值直接移动到EDX
寄存器中,而不是设置 0 或 1,然后必须将其操作为我们想要的值。因此,编译器将使用CMOV
指令(如前所述,当针对支持它们的处理器时)来实现更复杂的逻辑,例如
(a >= 13) ? 125 : -8
即使他们可以使用其他方法之一来做到这一点。当条件两端的操作数不是编译时常量(即,它们是寄存器中的值,仅在运行时知道)时,您还需要条件移动。
这有帮助吗? :-)
我试图看看 gcc 为这个代码示例吐出了什么,但不可能从它抛出的所有额外垃圾中挑选出逻辑。
是的。我有几个提示给你:
将代码缩减为一个非常简单的函数,只做你想学习的内容。您需要将输入作为参数(以便优化器无法简单地折叠常量),并且您需要返回函数的输出。例如:
int Foo(int a) { return a >= 13; }
返回
bool
在这里也可以。如果您使用条件运算符返回 0 或 1 以外的内容,则需要返回int
,当然。无论哪种方式,现在您都可以准确地看到编译器正在生成哪些汇编指令来实现这一点,而没有任何其他噪音。确保您已启用优化;查看调试代码没有指导意义,而且非常嘈杂。
确保您要求 GCC 使用 Intel/MASM 格式生成程序集列表,该格式比其默认格式 GAS/AT&T 语法更容易阅读(在我看来至少如此)。我上面的所有汇编代码示例都是使用英特尔语法编写的。所需的咒语是:
gcc -S -masm=intel MyFile.c
其中,
-S
为输入源代码文件生成程序集列表,-masm=intel
将程序集列表语法格式切换为英特尔样式。使用像Godbolt编译器资源管理器这样的好工具,它可以自动执行所有这些操作,从而大大减少周转时间。作为另一个好处,它对汇编指令进行颜色编码,以与原始源代码中的 C 代码行相匹配。
下面是一个你用来研究这个的例子。原始来源在最左边。中间窗格显示了现代处理器的 GCC 7.1 程序集输出,该处理器支持
CMOV
指令。最右窗格显示了不支持CMOV
指令的非常旧的处理器的 GCC 7.1 程序集输出。很酷,对吧?您可以轻松操作编译器开关并观察输出如何变化。例如,如果您执行-m64
(64 位)而不是-m32
(32 位),那么您会看到参数是在寄存器中传递的 (EDI
),而不是在堆栈上传递并且必须作为函数中的第一条指令加载到寄存器中。