那么,超低级的 IF() 是什么样的,x86 处理器如何处理它?
7 回答
处理器具有“Branch if”指令,当满足某个条件时它会分支,否则它会继续执行下一条指令。
所以
if(A)
{
dosomething;
}
会成为
load A into register 0
if the zero flag is set (ie, register 0 contains 0x00) then jump to endcondition)
dosomething
endcondition:
更复杂的条件 ( if(A || B && C)
) 成为使寄存器处于 0 或非零状态的指令序列,因此 branchif 指令可以根据条件标志跳转或不跳转。
有许多条件标志(零、进位、负数、溢出等),并且一些 branchif 指令也在更复杂的条件下运行(即,它实际上可能会检查一个寄存器是否等于另一个寄存器,而不是简单地查看标志)。每种架构都是不同的,并且需要权衡取舍,因此指令集是完整的,而且速度快且紧凑。
正如 moocha 在评论中指出的那样,某些架构允许您对一些、许多甚至所有指令应用条件,因此您可能不仅有“分支 if”指令,还有“and if”、“add if”、 '移动如果'等
一旦您进入流水线、乱序执行、缓存、微代码和所有其他高级主题,x86 就非常、非常、非常复杂,超出了这个简单的解释。对于大多数目的,上述解释就足够了。但是,如果您正在编写一个手工制作的非常非常紧密的算法,那么您必须考虑这些因素以获得最大的性能和吞吐量。
这是另一个问题的主题......
-亚当
使用 C 编译器的输出(使用-S
gcc 上的开关)来查看给定的 C 代码片段在编译时会生成什么输出是相当容易的。不过,在玩具程序上使用优化时要小心。如果您不小心,优化器通常会优化掉总是以一种或另一种方式运行的条件(有关更详细的解释,请参阅这篇关于微基准的文章)。
例如,一个简单的 C 程序:
#include <stdio.h>
int main (int argc, char **argv) {
int ii = 10;
int jj = 20;
if (jj > ii) {
puts ("jj > ii \n");
}
return 0;
}
编译为以下汇编语言:
.file "foo.c"
.section .rodata
.LC0:
.string "jj > ii \n"
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
subl $20, %esp
movl $10, -8(%ebp)
movl $20, -12(%ebp)
movl -12(%ebp), %eax
cmpl -8(%ebp), %eax
jle .L2
movl $.LC0, (%esp)
call puts
.L2:
movl $0, %eax
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2"
.section .note.GNU-stack,"",@progbits
简要剖析正在发生的事情:
第一部分 ( ) 用字符串 ' '
.rodata
声明一个常量jj > ii \n
第二部分是初始化堆栈上的
ii
和jj
变量的内容。来自的位
cmpl -8(%ebp), %eax
正在做实际的比较;该jle
指令跳过了对“puts
”的调用,这实际上是“if
”语句的逻辑颠倒了。在标签 '
.L2
' 之后,系统正在整理堆栈顶部并从调用中返回。
这是一个分支指令,取决于特定的机器架构。它计算出如何设置内存位置或寄存器来测试特定的低级条件 - 例如分支如果不等于或分支如果不为零,...... - 然后该测试是否会跳转(或者如果条件失败则不这样做)到内存的另一部分。显然,如果您有一个复杂的条件,它可能需要评估许多不同的条件,并且可能涉及多个分支指令。
通常,CPU 具有所谓的指令寄存器,它保存当前机器语言操作码的内存地址,以便接下来执行......以及许多其他寄存器来保存数据。
通常,cpu 执行指令寄存器中的每个操作码后,它只需将其加一以移动到内存中的下一个位置,该位置应该在已编译的程序应用程序中具有下一个操作码。
一个操作码(实际上可能有几个),但是通过“比较”其他两个 cpu 寄存器中的值,它允许 cpu “分支”,如果一个大于另一个,它将一个内存地址复制到指令寄存器中,而如果另一个是最大的,它会将第二个不同的内存地址复制到指令寄存器中。
这大约是“低”水平,因为它可以说是没有谈论继电器和晶体管......
以下是关于如何在 x86 架构上编译此类结构的一个很好的概述:http ://en.wikibooks.org/wiki/X86_Disassembly/Branches#If-Then
有时有一些方法可以避免分支(由于管道中断,这通常会对性能产生强烈的负面影响)。例如,从 i686 开始的指令集(从 Pentium Pro 到今天的所有指令)都有一个条件移动指令,它可能会编译这个:
if (a==0) {
b= 1;
}
像这样:
cmp 0, eax
cmovzl ebx, 1
没有分支,只要您的编译器设置为针对 i686+(感觉就像这样;编译器复杂且难以理解)。SET[condition] 是另一个类似的条件指令。
幸运的老 ARM 程序员可以有条件地指定任何指令,这大大减少了分支。
尽管大多数 if 语句最终会成为条件分支,但对于两个分支都没有任何副作用的非常简单的情况,优化编译器可能会生成执行这两个分支并计算结果的代码,而不是只执行一个。这在流水线架构中具有优势,其中计算两个分支的平均成本低于由于分支预测未命中而导致的平均成本。
例如,代码:
int x;
if ( y < 5 )
x = 5;
else
x = y;
可以像这样编译:
y -= 5
int r = y < 0; // r is 1 if y < 5, 0 otherwise
r -= 1 // r is 0x00000000 if y < 5, 0xffffffff otherwise
x = y & r // x is 0 if y < 5, (y-5) otherwise
x += 5; // x is 5 if y < 5, y otherwise
可以在没有任何分支的情况下转换为机器码
基本上,您有一堆电子在 CPU 内部的各种原子之间传递。由于 CPU 中硅原子的结构,电子遵循某些路径,这决定了计算机将遵循的执行分支。
编辑:看来我应该解释得不那么含糊了。忍耐一下,我学的是计算机科学,不是电气工程,所以我对这些东西没有很深的了解:
您的 CPU 由一种材料制成,通常是硅,称为“半导体”。半导体的一大优点是它们的电气特性可以通过“掺杂”轻松改变,或者应用杂质在材料上形成负或正“电荷载流子”区域。这些区域汇合在一起的线路被称为“交汇点”,电流通过这些交汇点的一种方式比另一种更容易。这一特性被用来制造二极管,它允许电流仅在一个方向上流动,以及晶体管,它可以被认为是允许一个电流控制另一个电流的微型开关。这些晶体管和二极管以多种方式组合在一起,以创建 CPU 的逻辑门。
CPU 内部的许多逻辑门都专门用作“控制单元”,它负责检索和解码指令,告诉 CPU 的其余部分做什么,最后得到下一条指令。在 x86 上,控制单元实际上正在运行“微代码”,它告诉它如何处理分支、流水线等。您确实需要非常具体地了解特定的处理器系列,才能了解 x86 ISA 如何在特定的微架构上实现。