1.优化程序性能的必要性?
2.为什么编译器有优化选项,而我们还要了解如何用我们自己的代码去优化?
3.优化程序性能都有哪些方法?
这边文章我将从以上三个方面去探讨如何从机器级角度去优化程序的性能~
1.优化程序性能的必要性?
2.为什么编译器有优化选项,而我们还要了解如何用我们自己的代码去优化?
我们在编译的时候可能会用到优化选项(-On),如果优化选项过高,编译出来的代码将很难调试,如果优化选项过低,优化效果可能不明显。一般情况下我们在编译工程的时候优化选项都会选择O2级别。但是有一点需要我们程序员明白,编译器给你的代码优化与否,取决于编译器对你代码的分析。如果编译器分析后得出你的代码没有二义性,那么编译器会替你优化代码。但是一般情况下,我认为编译器很难对你的代码进行优化,比如下面的例子:
int sum(int *a, int *b)
{
*a += *b;
*a += *b;
return *a;
}
int sum(int *a, int *b)
{
*a += 2 * (*b);
return *a;
}
乍一看这两个接口表达的意思是一样的,并且下面的性能要比上面的性能高,但是很多情况下,如果你用了上面的那个方法,编译器是不给你优化的,为什么?如果a和b指向同一块内存,这两个接口返回的结果是不一样的。因为编译器无法分析出你用这个接口的时候能不能保证两个参数是不是指向同一块内存,所以编译器就不会给你优化。
所以,我们不能完全依赖编译器的优化选项,还需要在我们自己的代码中去尽可能的优化。
3.优化程序性能都有哪些方法?
这里我将简单列出我目前掌握到的代码性能优化方法,并简单的说明,如果大家有什么问题的话,我们可以一起探讨共通学习。
有些背景知识需要简单说明一下:现在的cpu都是流水线式的,也就是说cpu不是等待一个指令完成了才去执行第二个指令的,而是可以并行的。
这是因为指令和指令之间不一定会有数据关联,没有数据关联的影响,指令就可以并行处理了。另外cpu执行指令分为ICU和EU两部分。ICU为指令控制单元,EU为指令执行单元。
1.消除不必要的寄存器引用,用临时变量存放结果代替入参
现有以下两个接口:
int dest[20] = {
1,2,3,4,5,
6,7,8,9,10,
11,12,13,14,15,
16,17,18,19,20
};
void combine_1(int size, int* dest, int* result)
{
for (int i = 0; i < 20; i++){
*result = dest[i] * *result;
}
}
void combine_2(int size, int* dest, int* result)
{
int res = 1;
for (int i = 0; i < 20; i++){
res = dest[i] * res;
}
*result = res;
}
结论:combine_2性能优于combine_1.
为什么2会由于1,请看下面的汇编代码:
*result = dest[i] * *result;
004113E6 mov eax,dword ptr [i]
004113E9 mov ecx,dword ptr [dest]
004113EC mov edx,dword ptr [result]
004113EF mov eax,dword ptr [ecx+eax*4]
004113F2 imul eax,dword ptr [edx]
004113F5 mov ecx,dword ptr [result]
004113F8 mov dword ptr [ecx],eax
res = dest[i] * res;
0041144D mov eax,dword ptr [i]
00411450 mov ecx,dword ptr [dest]
00411453 mov edx,dword ptr [ecx+eax*4]
00411456 imul edx,dword ptr [res]
0041145A mov dword ptr [res],edx
显然1比2多了两条mov操作。
2.减少过程调用。
也就是说尽量少的进行接口调用。但是这项优化是要付出代价的:这破坏了代码的模块性和抽象性。
3.消除循环低效率,循环条件式不应该调用方法。
例如for(int i = 0; i < get_max(); i++)需要改写成int j = get_max();for(int i = 0;i < j;i++)
下面两个方法为进阶的方法:
4.循环展开。将原来的2n次循环变为n次循环,并且可以用多个临时变量进行并行处理。
void combine_3(int size, int* dest, int* result)
{
int res_1 = 1;
int res_2 = 1;
for (int i = 0; i < 10; i = i + 2){
res_1 = dest[i] * res_1;
res_2 = dest[i + 1] * res_2;
}
*result = res_1 * res_2;
}
我们将combine_2再次进行优化,得到了combine_3,我们可以看一下汇编:
res_1 = dest[i] * res_1;
004114B4 mov eax,dword ptr [i]
004114B7 mov ecx,dword ptr [dest]
004114BA mov edx,dword ptr [ecx+eax*4]
004114BD imul edx,dword ptr [res_1]
004114C1 mov dword ptr [res_1],edx
res_2 = dest[i + 1] * res_2;
004114C4 mov eax,dword ptr [i]
004114C7 mov ecx,dword ptr [dest]
004114CA mov edx,dword ptr [ecx+eax*4+4]
004114CE imul edx,dword ptr [res_2]
004114D2 mov dword ptr [res_2],edx
虽然说combine_3的汇编跟combine_2是一样的,但是我们知道,循环跳转由原来的2n次变为了n次,并且,我们的cpu是流水线式的可以进行并行处理的。
所以combine_3的性能是比combine_2更高的。
5.条件转移指令替换条件指令,减少预测惩罚。比如用条件表达式替换if语句。
cpu在执行指令的时候,为了提高执行效率,会对执行分支进行预测。虽然预测的成功率很高,但是一旦预测失败将收到44个时钟周期的处罚(以core i7为例,预测失败会将
之前预测得到的值清空,再重新执行另外的分支)。所以预测惩罚还是很影响效率的。