人在道路的分岔口时要预测哪条路能够到达目的地,面对众多选择时,计算机也一样要抉择,毕竟计算机的运行方式是以人的思路来设计的,计算机中的抉择其实就是人在抉择。
cpu中的指令是在流水线上执行。分支预测,是指当处理器遇到一个分支指令时,是该把分支左边的指令放到流水线上还是把分支右边的指令放在流水线上呢?
如C语言程序中的if、switch、for等语言结构,编译器将它们编译成汇编代码后,在汇编一级来说,这些结构都是用跳转指令来实现的,所以,汇编语言中的无条件跳转指令很丰富,以至于称之为跳转指令“族”,多的足矣应对各种转移方式。
举个例子,如下面测试代码
1 void main () {
2 int i = 0;
3 while (i < 10) {
4 i++;
5 }
6 }
里面的while结构,就是执行了10次i++。我们来看一下while结构是如何翻译成汇编语言的。
gcc -S -o ~/test/while.S ~/test/while.c回车,这样gcc就将while.c编译成了汇编代码while.S。其中的参数-S是编译到汇编语言,不进行汇编和链接。
查看下~/test /while.S文件,cat -n ~/test/while.S回车
1 .file "while.c"2 .text3 .globl main4 .type main, @function5 main:6 pushl %ebp7 movl %esp, %ebp8 subl $16, %esp9 movl $0, -4(%ebp)
10 jmp .L2
11 .L3: ;此处是while的循环体
12 addl $1, -4(%ebp)
13 .L2: ;此处是while循环条件表达式
14 cmpl $9, -4(%ebp)
15 jle .L3
16 leave
17 ret
18 .size main, .-main
19 .ident "GCC: (GNU) 4.4.6 20120305 (Red Hat 4.4.6-4)"
20 .section .note.GNU-stack,"",@progbits
这个生成的汇编语言并不是我们熟悉的intel语法,而是AT&T语法,如果此时您觉得太陌生也不要慌张,因为在后面的章节我们会专门说到此类语法,现在先抛出来和大家预预热。
本来打算只列出第9~15行的,但考虑到本身才20行,干脆就全贴出来了,简要说明下,前4行是用于声明代码段、导出main函数符号。第5行是main函数起始地址,高级语言中的函数名在汇编语言中只是个符号,而符号便是地址,这就是很多教科书上都说函数名是地址的原因。话说数组也同理,数组名在汇编语言中也是个标号地址,所以数组名也是地址。局部变量是在栈中分配空间的,所以第6~8行是在创建堆栈框架,也就是为局部变量i在栈中分配空间,-4(%ebp)便是指局部变量i。堆栈框架以后会说到。咱们主要是看第9~15行。
第9行是为变量i赋值为0。AT&T语法中,寄存器前要用%来指示,立即数前要用$来指示。-4(%ebp)表示内存地址“ebp寄存器的值减4”处内存内容。相当于intel汇编语法形式[ebp – 4]。AT&T语法中是源操作数在左,目的操作数在右,和intel语法相反。所以第9行是将0送入了变量i所在的栈空间。
第10行就是简单的无条件跳转,直接进入while循环结构的条件表达式判断,也就是第13行。
第14行就是while括号中的条件表达式,用变量i的值和立即数9做比较。
第15行的jle意思是,若第14行的比较结果是小于等于9,则跳到11行,继续执行第12行的加法。可见第11~12行则是循环体。
程序执行流是由第15行跳到第11行,这样组成了循环结构的回路。
程序执行while循环后就结束了,所以局部变量i所在的栈空间要被回收,第16行的指令leave是用于堆栈框架的回收工作。
第17行是main函数退出。由于main也是被调用的,所以gcc显示的帮咱们加了个ret以示退出,为什么main也是由别人调用的,这个在加载用户程序时咱们会说到的。
上面的第15行jle指令就是程序中的分支结构。我们花了“大力气”讲述了程序流的分支,这并不是浪费力气。类似这样的分支结构很多,它们只有两种结果,要么转移到这一边,要么转移到那一边。分支结构虽然让程序更加灵活多样,但这却成了cpu执行效率的诟病。这是怎么回事呢?
之前说流水线的时候,我和大家强调了两次“重叠”,即同一时间周期内完成的是当前指令的执行,下一条指令的译码,第三条指令的取指。其中最重要的是“执行”,指令只有执行了,才真正是泼出去的水,收不回来了。另外的译码和取指并不重要,首先它们并不是执行,其次它们也不属于当前指令,当前指令的“取指”和“译码”早就在前两个周期内完成了。
不知道您注意到了没有,拿表4-14的周期3来说,这一时钟周期内的“执行”是指的当前指令的执行阶段,“取指”和“译码”这两个工序分别隶属于未来要执行的下一条指令和下下一条指令。想到这里不禁要有个疑问,这两个未来的指令,cpu是如何确定的?如果程序一直是顺序执行的,未来无论多少条指令都可以轻易得到,都可以提前放到流水线上。可是,程序是有分支啊,到底该把哪个分支的指令放到流水线上呢?
流水线是有效提升cpu效率的方式,但流水线最大的问题是程序中的分支结构,如何把握好转移的方向,才是使流水线保持高效的关键,因为如果流水线上的指令放错了的话,必须要清空那些已经在流水线上的指令,一定不能执行错误的指令。随着流水线级数越多,要清空的指令也将越多,清空流水线的代价就越大,这严重影响cpu效率。
当遇到一个分岔口时,是往左走还是往右走呢?对于这种分支情况,就需要预测出哪一侧的指令将被执行,然后将预测出的那一分支上的指令放入流水线。从统计学的角度来看,某些事情一旦出现,下一次出现的机率还会很大。纵观历史,很多事情都是在重复的发生,很多伟人都拿这些历史样本来预测未来发生的事情。这个说的有点悬乎了,说点简单的,比如现在是葡萄收获的季节,今天刚吃了葡萄,很好吃,明天后天甚至未来的几周都会继续吃葡萄,哈哈,我大爱葡萄。
本内容摘自《操作系统真象还原》,作者不容易,请大家支持正版。