大概所有学习C语言的初学者,都被前辈说过,C语言是世界上接近最速的编程语言,当然这并不是吹牛,也并不是贬低其他语言,诚然非C语言能写出高速度的代码,但是C语言更容易写出高速的程序(高速不代表高效),然而再好的工具,在外行人手中也只能是黯淡没落。
对于现代编译器,现代CPU而言,我们要尽量迎合CPU的设计(比如架构和处理指令的方式等),虽然编译器是为程序员服务,并且在尽它最大的能力来优化程序员写出的代码,但是毕竟它还没有脱离电子的范畴,如果我们的代码不能让编译器理解,编译器无法帮我们优化代码,那么我们就无法写出一个高速的程序。
对于此,我们可以暂且忽略CPU的设计,因为我们在层面上只能考虑如何迎合编译器的优化规则,而CPU则是语言以及编译器的事情了。
提高程序的速度,就C语言而言可以有这几种方法:
首先还是要设计合理的大纲,正所谓一个程序最大的性能提升就是它第一次运行的时候
要避免连续的函数调用。
- 消除不必要的存储器使用(并非推荐使用register)
- 使用循环展开技巧,一般编译器的优化选项能自动帮你修改代码成循环展开
- 对于一个操作的核心耗时部分,通过重新组合技术来提高速度
- 多采用几种风格的写法,而不是直观的认为,因为计算机的想法和你是不一样的
注:随着编译器的版本更新,即使不开启优化选项,自带的编译器优化依旧能够为我们编写的代码提供一部分优化,这便是不使用老版本编译器的原因,虽然作为一个程序员不应该太依赖于编译器,但是我认为,时代在进步,信息量正在无限的膨胀,但是人类的大脑以及精力在一个大时代内是有限的,换句话说对于普通人而言我们的记忆是有限的,我们不应该把精力放在前人已经做完的事情上,而是要站在巨人的肩膀上向更远处眺望,如此我们应该充分利用工具来帮助我们实现一些既有的功能,而程序员应该更 专注于发现新的思路,以及想法,在图灵测试尚未有人打破之前,程序员依赖编译器并不是一件错误的事情。
对于当下的编译器,以GCC(GCC不仅仅是一个编译器,但这里将它当成编译器的代名词)为例,-O2是一个为大众所接受的优化等级,对于其他编译器,一般程序员可以选择使用由Google和Apple联合开发的编译器clang也是一个很好的选择, 在-O2的优化等级下,GCC一般情况下能够自动执行循环展开优化,
开始.
/*struct.h*/ #include <stdio.h> typedef struct me{ int value; struct me* next; }data_t; typedef struct{ int index; data_t* storage; }block;</div>
为了测试方便我们首先定义了两个结构体,分别是:
block代表一个块,每个块都有一个序号(int),一个数据域data_t
data_t代表一个数据域,原型是一个链表,每个data_t对象中包含一个数据和一个指针。
/*main.c*/ #include "struct.h" #define ARR_SIZE 10 static inline int get_len(const data_t* data) { int len = 0; if(!data) fprintf(stderr,"The data in %p is NULL\n",data); else while(!data->next) { ++len; data = data->next; } return len; } static inline void mix_cal(const block* process, int result[]) { for(int i = 0;i < get_len(process->storage);++i) { *result += (process->storage)[i]; } }</div>
此时我们得到了两个测试函数,get_len和mix_cal分别用来得到data_t长度,以及计算数据域的总和。
/*main.c*/ int main(void) { block* block_in_all[ARR_SIZE] = { NULL }; int result_in_all[ARR_SIZE] = { 0 }; /* * 假设生成了许多的`block`类型对象 * 将许多的`block`放置在一个数组中,每个元素类型为`block*` * 每个block对象中都包含非空的data_t类型的数据域 */ for(int i = 0;i < ARR_SIZE;++i) { mix_cal(block_in_all[i], result_in_all+i); } for(int i = 0;i < ARR_SIZE;++i) { printf("The %dth block have the total %d data\n", block_in_all[i]->index, result_in_all[i]); } return 0; }</div>
耐心读完上述的代码,它是用来求和的,求一个域中的所有元素的和。仔细分析一下,很容易就能看见一些缺点,最大的莫过于在mix_cal函数中对于get_len函数的调用,在此处看来十分明显,但是我们在编写程序的时候是否能够注意到这个问题呢?
对于一些不必要的函数调用我们要做的便是将他们提取出来,使用临时变量是一个很好的办法,因为在编译器的帮助下临时变量在允许的情况下能够充分的利用CPU的寄存器。之所以是允许的情况下,是因为寄存器的数量并不多,而编译器在寄存器的使用上需要考虑许多的复杂因素,故并不是每次使用临时变量都能加入寄存器。但这并不妨碍我们提升程序的性能。
在此处,我们应该将for循环中的判断语句里的get_len函数提取出来,在外部使用一个临时变量接收结果,而不是在循环中一直调用该函数。
int len = get_len(process->storage);</div>
.
依旧是上方的代码,我们来讲述一下,循环展开。
对于mix_cal函数,我们或者说编译器可以如何提升它的速度呢?我们说过一点的小改变都可能对一个程序的最终代码产生极大的影响,对此最常用的便是尝试,前人之路早已铺好,不需要重复造轮子了。
循环展开:
int reality = len - 1, i; for(i = 0;i < reality;i+=2) { *result = *result + (process->storage)[i] + (process->storage)[i+1]; } for(;i < len;++i) { *result += (process->storage)[i]; }</div>
这就是循环展开中的2次循环展开,同样还有n次循环展开。
同样,在刚才提到过寄存器的使用以及减少不必要的开销,在此程序中对于(process->storage)[i]这样的存储器位置解引用太过浪费,我们总是将其优化成本低临时变量的使用
data* local_data = process->storage;</div>
这将为程序带来十分可观的节约,虽然这些工作在编译器的优化中都能包括,但是一旦我们的代码难以被编译器所理解(虽然编译器的升级最大的目的就是提升优化效果),那么我们很可能得到一个性能不够可观的程序。所以当我们并不是特别紧凑的时候,可以将这些工作当成我们的本分来做,而不是交给编译器来做。
以及对于外部存储位置 result 我们在此处也是存在着浪费,同样我们应该使用一个临时变量来存储总和,而不是每次得到结果便对它进行解引用操作。
int local_result = 0; /*...*/ local_result = local_result + local_data[i] + local_data[i+1]; /*...*/ *result = local_result;</div>
在上方我们可以看见循环展开被称作2次循环展开,那么自然可以推断有n次循环展开,自然是有的,对于一个n次循环展开的式子我们有一个简便的上界确定公式即:
reality = len - n + 1;</div>
至于展开几次最好,依然是视环境而定。 故最终的版本应该为:
static inline void mix_cal(const block* process, int result[]) { int local_result = 0; int len = get_len(process->storage); int reality = len - 1, i; data* local_data = process->storage; for(i = 0;i < reality;i+=2) local_result += local_data[i] + local_data[i+1]; for(;i < len;++i) local_result += local_data[i]; *result = local_result; }</div>
解释:循环展开将元素相加分为两个部分,第一部分每次加两个元素,由于如此做会剩余元素没有加,故在第二部分将剩下的元素都加起来。
. 还有一种叫做重新组合的技巧,即为让一个表达式中的运算数自由组合,组合出最快速的一种,但是这种方法未曾试验过。故不提及。
. 对于条件分支预测错误造成的时间损耗,称之为惩罚,最通俗的说法,就是当你编写的代码中含有条件分支的时候,处理器会选择去预判某一个分支是此次正确的支路,这样可以避免修改任何实际的寄存器和存储器,一直到确定了实际结果,要是不对,那就惨了,这段时间做的事情都白费了。但是也不必过分的关心这种条件分支的预测,这也是我放在最后说的意义所在。
这里有两种较为客观的方法,一种被称为命令式,一种被称为功能式
命令式:
for(int i = 0;i < n;++i) { if(a[i] > b[i]){ int temp = a[i]; a[i] = b[i]; b[i] = temp; } }</div>
功能式:
int min, max; for(int i = 0;i < n;++i) { min = a[i] < b[i] ? a[i] : b[i]