据我了解,cmp指令将设置标志寄存器中的一些位。然后,您可以使用jle,jnp等指令进行分支。
我想知道的是如何从比较中恢复积分值。
示例:以下是有效的c语法
y = x[a >= 13];
因此将a与13进行比较以获得真或假,其分别被解释为1或0。但是,必须将1或0作为整数馈入数组访问。编译器会做什么?
我能想到的一些事情是:
进行比较,然后分支到x [0]或x [1]
进行比较然后分支执行tmp = 0或tmp = 1然后执行x [tmp]
也许在标志上做一些奇特的逻辑(不确定是否有直接访问标志的指令)
我试图查看gcc为此代码示例吐出的内容,但是无法从所引发的所有额外垃圾中挑选逻辑。
我正在研究编译器,所以任何建议都会受到赞赏。
答案 0 :(得分:3)
基本上有三种方法可以做到这一点。我会一次过一次。
实现它的一种方法基本上就是你在问题中描述的内容:进行比较,然后分支到单独实现两种可能性的代码。例如:
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
表示"如果大于或等于,则跳转#34 ;;类似地,setge
表示"设置是否大于或等于"。简单。
关于SETcc
的诀窍在于它设置了一个BYTE大小的寄存器,它基本上意味着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.事实证明将整个寄存器预先归零为the most optimal way of doing this on all processors,但还有其他方法,例如在MOVZX
指令之后使用SETcc
。关于这个问题的链接答案非常详细,所以我不会在这里讨论它。关键点是SETcc
只设置寄存器的低字节,但后续指令要求整个 32位寄存器具有该值,因此您需要消除垃圾中的垃圾。高位字节。
无论如何,这是编写器在编写类似y = x[a >= 13]
之类的时候会产生99%的代码的代码。 SETcc
指令为您提供了根据一个或多个标志的状态设置字节的方法,就像您可以在标志上分支一样。这基本上就是你想到的一条允许直接访问标志的指令。
这实现了
的逻辑(a >= 13) ? 1 : 0
但如果你想做什么
(a >= 13) ? 125 : -8
像我之前提到的那样?好吧,你仍然使用SETcc
指令,但是你做了一点点花哨的事情,然后去#34;修复"得到的0或1到你真正想要的值。例如:
xor edx, edx
cmp [a], 13
setge dl
dec edx
and dl, 123
add edx, 125
; do whatever with EDX
这几乎适用于任何二元选择(两个可能的值,具体取决于条件),优化编译器足够聪明,可以解决这个问题。仍然无分支代码;非常酷。
第三种方式可以实现,但它在概念上非常类似于我们刚才谈到的第二种方式。它使用条件移动指令,这是根据flags状态执行无分支集合的另一种方法。条件移动指令为CMOVcc
,其中cc
再次引用"条件代码",与前面示例中的完全相同。 CMOVcc
指令是在大约1995年的Pentium Pro中引入的,并且从那以后一直在所有处理器中(好吧,不是Pentium MMX,而是Pentium 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语法。上面的所有汇编代码示例都是使用Intel语法编写的。所需的咒语是:
gcc -S -masm=intel MyFile.c
其中-S
生成输入源代码文件的程序集列表,-masm=intel
将程序集列表语法格式切换为Intel样式。
使用像Godbolt Compiler Explorer这样的漂亮工具,它可以自动完成所有这些工作,从而大幅减少周转时间。作为另一个奖励,它对汇编指令进行颜色编码,以与原始源中的C代码行匹配。
Here is an example of what you'd use to study this。原始来源位于最左侧。中间窗格显示了现代处理器的GCC 7.1程序集输出,该处理器支持CMOV
指令。最右边的窗格显示了GCC 7.1的程序集输出,这是一个非常老的处理器,它不支持CMOV
指令。很酷,对吗?您可以轻松地操作编译器开关并观察输出如何变化。例如,如果您执行-m64
(64位)而不是-m32
(32位),那么您将看到参数在寄存器中传递(EDI
),而不是作为函数中的第一条指令被传递到堆栈并且必须加载到寄存器中。