在阅读csapp这本书中关于第五章优化程序性能——条件数据传送和条件控制转移时,我陷入了一个误区就是:条件数据传送方法的关键汇编指令必然是cmov,条件控制转移方法的关键汇编指令必然是cmp。但对两种不同的方法编译后产生的汇编代码进行分析时却发现这个观点是错误的。
1.条件控制转移
根据条件的成立来决定程序的执行流程,涉及到分支操作。同时因为它的分支预测收益高,所以现在指令的流水线架构通常为它提供分支预测来提高综合性能。接下来的分析也会解释为什么它的收益。
//条件控制转移 a数组中i下标的所有元素坐标要比b小
void ControlTransfer(long a[],long b[],long n){
for(long i = 0;i<n;i++){
if(a[i] > b[i]){
long t = a[i];
a[i] = b[i];
b[i] = t;
}
}
}
得到的汇编代码如下所示,关键指令已标出。
0x004015c4 <+0>: push %ebp
0x004015c5 <+1>: mov %esp,%ebp
0x004015c7 <+3>: sub $0x10,%esp //esp = esp - 16byte
=> 0x004015ca <+6>: movl $0x0,-0x4(%ebp) // i =0
0x004015d1 <+13>: jmp 0x401647 <ControlTransfer+131> //判断i<n
0x004015d3 <+15>: mov -0x4(%ebp),%eax // eax = i
0x004015d6 <+18>: lea 0x0(,%eax,4),%edx // 4i 此时为0
0x004015dd <+25>: mov 0x8(%ebp),%eax //取出基值 *a
0x004015e0 <+28>: add %edx,%eax //eax = *a + 4i => a[0]
0x004015e2 <+30>: mov (%eax),%edx //edx = *a 解析a[0]的值放入edx中
0x004015e4 <+32>: mov -0x4(%ebp),%eax //eax = i (0)
0x004015e7 <+35>: lea 0x0(,%eax,4),%ecx //ecx = 4i = 0
0x004015ee <+42>: mov 0xc(%ebp),%eax // 8(%ebp) + offset(4byte) = c(%ebp) 即*b
0x004015f1 <+45>: add %ecx,%eax //eax = *b + 4i => b[0]
0x004015f3 <+47>: mov (%eax),%eax //eax = *b 即b[0]
0x004015f5 <+49>: cmp %eax,%edx //比较eax=a[0]和ebx = b[0] !--关键指令 --!
0x004015f7 <+51>: jle 0x401643 <ControlTransfer+127> //a[0] <= b[0] 条件码为小于等于时触发条件 jmp到0x401643判断循环是否越界
0x004015f9 <+53>: mov -0x4(%ebp),%eax //a[0]>b[0] 条件成立.开始交换a[0]和b[0]
0x004015fc <+56>: lea 0x0(,%eax,4),%edx //edx = 4*0
0x00401603 <+63>: mov 0x8(%ebp),%eax //eax = *a
0x00401606 <+66>: add %edx,%eax
0x00401608 <+68>: mov (%eax),%eax //eax = a[0]
0x0040160a <+70>: mov %eax,-0x8(%ebp) //t = a[0]
0x0040160d <+73>: mov -0x4(%ebp),%eax //eax = 0
0x00401610 <+76>: lea 0x0(,%eax,4),%edx //edx = 4 * 0
0x00401617 <+83>: mov 0x8(%ebp),%eax //eax = *a
0x0040161a <+86>: add %eax,%edx //edx = *a + 4*0
0x0040161c <+88>: mov -0x4(%ebp),%eax
0x0040161f <+91>: lea 0x0(,%eax,4),%ecx
0x00401626 <+98>: mov 0xc(%ebp),%eax
0x00401629 <+101>: add %ecx,%eax
0x0040162b <+103>: mov (%eax),%eax
0x0040162d <+105>: mov %eax,(%edx) //a[0] = b[0]
0x0040162f <+107>: mov -0x4(%ebp),%eax
0x00401632 <+110>: lea 0x0(,%eax,4),%edx
0x00401639 <+117>: mov 0xc(%ebp),%eax
0x0040163c <+120>: add %eax,%edx
0x0040163e <+122>: mov -0x8(%ebp),%eax //eax = t = a[0]
0x00401641 <+125>: mov %eax,(%edx) //b[0] = t;
0x00401643 <+127>: addl $0x1,-0x4(%ebp) //i+1
0x00401647 <+131>: mov -0x4(%ebp),%eax
0x0040164a <+134>: cmp 0x10(%ebp),%eax //ebp + 16byte = arr_size cmp n
0x0040164d <+137>: jl 0x4015d3 <ControlTransfer+15>
2.条件数据传送
根据条件的成立来传输数据,不涉及分支操作。因为它的分支预测收益极低,所以指令流水线架构并没有为它提供分支预测的必要,以为一旦预测失败造成的损耗也是很大的。利润低风险大,没必要。
//条件数据传送
void DataTransfer(long a[],long b[],long n){
for(long i = 0;i<n;i++) {
long min = a[i] < b[i]?a[i]:b[i];
long max = a[i] < b[i]?b[i]:a[i];
a[i] = min;
b[i] = max;
}
}
得到的汇编代码如下所示,关键指令已标出。
0x00401652 <+0>: push %ebp
0x00401653 <+1>: mov %esp,%ebp
0x00401655 <+3>: sub $0x10,%esp
0x00401658 <+6>: movl $0x0,-0x4(%ebp)
0x0040165f <+13>: jmp 0x4016e6 <DataTransfer+148> //jmp到越界判断
0x00401664 <+18>: mov -0x4(%ebp),%eax
0x00401667 <+21>: lea 0x0(,%eax,4),%edx
0x0040166e <+28>: mov 0xc(%ebp),%eax
0x00401671 <+31>: add %edx,%eax //eax =*a + 4 * 0
0x00401673 <+33>: mov (%eax),%eax //eax = a[0]
0x00401675 <+35>: mov -0x4(%ebp),%edx
0x00401678 <+38>: lea 0x0(,%edx,4),%ecx
0x0040167f <+45>: mov 0x8(%ebp),%edx
0x00401682 <+48>: add %ecx,%edx //edx =*b + 4 * 0
0x00401684 <+50>: mov (%edx),%edx //edx = b[0]
0x00401686 <+52>: cmp %edx,%eax //b[0] a[0] <!--- 关键指令 ---!>
0x00401688 <+54>: jle 0x40168c <DataTransfer+58> //b[0]<=a[0]
0x0040168a <+56>: mov %edx,%eax //a[0] = b[0]
0x0040168c <+58>: mov %eax,-0x8(%ebp) //-0x8(%ebp) = a[0]
=> 0x0040168f <+61>: mov -0x4(%ebp),%eax //eax = 0
0x00401692 <+64>: lea 0x0(,%eax,4),%edx //edx = 4*0
0x00401699 <+71>: mov 0x8(%ebp),%eax //eax = *b
0x0040169c <+74>: add %edx,%eax //eax = *b + 4*0
0x0040169e <+76>: mov (%eax),%eax //eax = b[0]
0x004016a0 <+78>: mov -0x4(%ebp),%edx //edx = 0
0x004016a3 <+81>: lea 0x0(,%edx,4),%ecx //ecx = 4*0
0x004016aa <+88>: mov 0xc(%ebp),%edx //edx = *a
0x004016ad <+91>: add %ecx,%edx
0x004016af <+93>: mov (%edx),%edx //edx = a[0]
0x004016b1 <+95>: cmp %edx,%eax //a[0] b[0]
0x004016b3 <+97>: jge 0x4016b7 <DataTransfer+101> a[0]>=b[0]
0x004016b5 <+99>: mov %edx,%eax //b[0] = a[0]
0x004016b7 <+101>: mov %eax,-0xc(%ebp) //-0xc(%ebp) = b[0];
0x004016ba <+104>: mov -0x4(%ebp),%eax //eax = 0;
0x004016bd <+107>: lea 0x0(,%eax,4),%edx //edx = 4*0
0x004016c4 <+114>: mov 0x8(%ebp),%eax //eax = *b
0x004016c7 <+117>: add %eax,%edx //*b+4*0
0x004016c9 <+119>: mov -0x8(%ebp),%eax //eax = -0x8(%ebp) => a[0]
0x004016cc <+122>: mov %eax,(%edx) //b[0] = -0x8(%ebp)
0x004016ce <+124>: mov -0x4(%ebp),%eax
0x004016d1 <+127>: lea 0x0(,%eax,4),%edx
0x004016d8 <+134>: mov 0xc(%ebp),%eax //eax = *a
0x004016db <+137>: add %eax,%edx
0x004016dd <+139>: mov -0xc(%ebp),%eax
0x004016e0 <+142>: mov %eax,(%edx)
0x004016e2 <+144>: addl $0x1,-0x4(%ebp) //i + 1
0x004016e6 <+148>: mov -0x4(%ebp),%eax //循环越界判断
0x004016e9 <+151>: cmp 0x10(%ebp),%eax
0x004016ec <+154>: jl 0x401664 <DataTransfer+18>
3.相隔总时钟周期数的不同
通过汇编可以发现,两种方法都使用了cmp指令,但是你可以根据汇编发现他们在cmp后,jmp跳过的指令数量不同。对于PIPE-指令流水线来说,也就意味着时钟周期相隔的差异。
对于条件控制传送来说:
0x004015f7 <+51>: jle 0x401643 <ControlTransfer+127>
当cmp判断结束后,若是条件成立,设置了相应的条件码,那么jmp将会从 0x004015f7 -》0x401643。从中间隔的内存大小不难看出,相隔了数条指令,也就是总时钟周期跨度相对另一种方法很大。
对于条件数据传送来说:
0x00401688 <+54>: jle 0x40168c <DataTransfer+58> //b[0]<=a[0]
当cmp判断结束后,若是条件成立,设置了相应的条件码,那么jmp将会从 0x00401688 -》0x40168c ,仅仅相隔了一个指令(4byte),也就是一个时钟周期的延迟。那么当cmp指令位于执行阶段时,jle则处于取指阶段。
4.后言
综上,这就是为什么即使两种方法使用了同样的汇编指令,但性能的差异是截然不同的。总时钟周期数不同所导致的。条件控制传送,若是分支预测成功,它可以优化中间许多条指令,得到的CPE(Cycles Per Element 每个元素的周期数)效率将提升,但预测错误的风险也高,需要跨过数个周期回到原点来。而条件数据传送,jmp的目标地址和源地址仅仅一个周期数的差距,没有优化的必要性,他很稳定,不需要分支预测。