当我们遇到这样情况的时候,是不是不知所措呢?怎么防止遇到这样的情况呢,我们来分析一下程序加速的一些方法。
硬件、编译器造成的
使用好点的电脑无疑是一个操作上的最佳选择,其次,对于编译器也是可以编译选项优化的,例如在VS环境中,可以通过配置属性来实现,具体步骤如下,大家可以参考:https://blog.csdn.net/yizhou2010/article/details/52635288
代码编写风格
多使用自加、自减指令和复合赋值表达式
你觉得使用i++ ,i = i + 1,i += 1有区别吗?我们来测试一下C代码:
- void asd() {}
- int main() {
- int i=0;
- i++;
- asd(); //方便区分上下文
- ii=i+1;
- asd();
- i+=1;
- return 0;
- }
反汇编:
- mov [rbp+i], 0 //i的初始化
- add [rbp+i], 1 //i++;
- call _Z3asdv ; asd(void)
- add [rbp+i], 1 //ii=i+1;
- call _Z3asdv ; asd(void)
- add [rbp+i], 1 //i+=1;
我们看到这个结果是一样的,但是在更加复杂的表达式中就会多生成几个指令了,而且用 i += 1 的,总是比写 i = i + 1的要稍微那么好看些。
除法换成乘法或者移位来表达
除法就是由乘法的过程逆推来的,依次减掉(如果x够减的)y^(2^31),y^(2^30),...y^8,y^4,y^2,y^1。减掉相应数量的y就在结果加上相应的数量,一般来说,更耗时间一些,用一个demo来测试一下
- auto time_start = std::chrono::system_clock::now();
- int iCount = 100000;
- double k ;
- for (int i = 0; i < 1000000; i++)
- {
- tmp = iCount / 2;
- }
- std::chrono::duration<double> time_spend = std::chrono::system_clock::now() - time_start;
- double test1 = time_spend.count() * 1000;
- cout<<"test1 cost "<<time_cost<<" ms"<<endl;
- time_start = std::chrono::system_clock::now() ;
- for (int i = 0; i < 1000000; i++)
- {
- tmp = iCount * 0.5f;
- }
- time_spend = std::chrono::system_clock::now() - time_start;
- test2 = time_spend.count() * 1000;
- cout<<"test2 cost "<<time_cost<<" ms"<<endl;
- time_start = std::chrono::system_clock::now() ;
- for (int i = 0; i < 1000000; i++)
- {
- tmp = iCount >>1;
- }
- time_spend = std::chrono::system_clock::now() - time_start;
- test3 = time_spend.count() * 1000;
- cout<<"test3 cost "<<time_cost<<" ms"<<endl;
我们输出结果会发现,移位和乘法比除法要省3-5倍时间,移位相对而言是最省时间的。
多用直接初始化,少用拷贝初始化
- string s1 = "hiya"; // 拷贝初始化
- string s2("hello"); // 直接初始化
- string s3(10, 'c'); // 直接初始化
当我们使用拷贝初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换,会浪费一定的资源时间,而直接初始化是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数和拷贝构造函数。
我们来看看Primer中怎么说的
当用于类类型对象时,初始化的复制形式和直接形式有所不同:直接初始化直接调用与实参匹配的构造函数,复制初始化总是调用复制构造函数。复制初始化首先使用指定构造函数创建一个临时对象,然后用复制构造函数将那个临时对象复制到正在创建的对象”
还有一段说到:
通常直接初始化和复制初始化仅在低级别优化上存在差异,然而,对于不支持复制的类型,或者使用非explicit构造函数的时候,它们有本质区别:
- ifstream file1("filename")://ok:direct initialization
- ifstream file2 = "filename";//error:copy constructor is private
局部变量、静态局部变量、全局变量与静态全局变量
- 局部变量是存在于堆栈中的,对其空间的分配仅仅是修改一次esp寄存器的内容即可;
- 静态局部变量是定义在函数内部的,静态局部变量定义时前面要加static关键字来标识,静态局部变量所在的函数在多调用多次时,只有第一次才经历变量定义和初始化;
- 当一个文件或者数据反复使用时,应该存储在全局变量中,避免重复加载使用;
- 静态全局变量是静态存储方式,静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。
静态变量是低效的,当一块数据被反复读写,其数据会留在CPU的一级缓存(Cache)中
代码冗余度
避免大的循环,循环中避免判断语句
在写程序过程中,最影响代码运行速度的往往都是循环语句,我记得当时在写matlab的时候,处理大数据,都是禁止用循环的,特别是多层嵌套的循环语句。
其次,尽量将循环嵌套控制在 3 层以内,有研究数据表明,当循环嵌套超过 3 层,程序员对循环的理解能力会极大地降低。同时,这样程序的执行效率也会很低。因此,如果代码循环嵌套超过 3 层,建议重新设计循环或将循环内的代码改写成一个子函数。
- for (i=0;i<100;i++)
- {
- for (j=0;j<5;j++)
- {
- for (j=0;j<5;j++)
- {
- /*处理代码*/
- }
- }
- }
多重 for 循环中,如果有可能,应当尽量将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数
- for (i=0;i<100;i++)
- {
- for (j=0;j<5;j++)
- {
- /*处理代码*/
- }
- }
改为:
- for (j=0;j<5;j++)
- {
- for (i=0;i<100;i++)
- {
- /*处理代码*/
- }
- }
逻辑判断不要在循环中使用,当 for 循环的次数很大时,执行多余的判断不仅会消耗系统的资源,而且会打断循环“流水线”作业,使得编译器不能对循环进行优化处理,降低程序的执行效率
- if (condition)
- {
- for (i = 0;i < n;i++)
- {
- /*处理代码*/
- }
- }
- else
- {
- for (i = 0;i < n;i++)
- {
- /*处理代码*/
- }
- }
尽量避免递归,递归就是不停的调用自身,所以非常消耗资源,甚至造成堆栈溢出和程序崩溃等等问题!
- int Func(int n)
- {
- if(n < 2)
- return 1;
- else
- return n*Func(n-1);
- }
因此,掌握循环优化的各种实用技术是提高程序效率的利器,也是一个高水平程序必须具备的基本功。
尽量不使用继承和多重继承
多重继承增加了类的继承层次的复杂性,调试难度增加当然风险也增加了,而且使用父类指针指向子类对象变成了一件复杂的事情,得用到C++中提供的dynamic_cast来执行强制转换。但是dynamic_cast是在运行期间而非编译期间进行转换的,因此会会带来一些轻微的性能损失,建议类型转换尽量采用c++内置的类型转换函数,而不要强行转换
少用模板,因为模板是编译期技术,大量采用模板也会增加编译时间
在c++primer3中,有一句话:
在多个文件之间编译相同的函数模板定义增加了不必要的编译时间简单点说,对于一个zhidaovector的函数,比如size(),如果在不同的cpp中出现,在这些文件编译的时候都要把vector::size()编译一遍。然后在链接的时候把重复的函数去掉,很显然增加了编译时间。模版函数需要在编译的时候实例化zhidao,所以呢,不把模版的实现代码放到头文件中的话(在头文件中实例化),那么每个使用到这个模版的cpp的都要把这个模版重新实例化一遍,所以增加了编内译时间
编码依赖性
声明与实现分离,删除不必要的#include
- 使用include时,只需要include这个接口头文件就好
- 并不是所有的文件都需要包含头文件 iostream,定义了输出函数引用就好
- ostream头文件也不要,替换为 iosfwd, 为什么,参数和返回类型只要前向声明(forward declared )就可以编译通过
尽量减少参数传递,多用引用来传递参数。
- bool func1(string s1, string s2)
- bool func2(string *s1, string *s2)
- bool func3(string &s1, string &s2)
指针和引用都不会创建新的对象,函数func2和func3不需要调用析构和构造函数,函数func1使用值传递在参数传递和函数返回时,需要调用string的构造函数和析构函数两次。
适当的采用PIMPL模式
很实用的一种基础模式,通过一个私有的成员指针,将指针所指向的类的内部实现数据进行隐藏。将实现放到CPP里,主要作用在于编译分离,其实是增加了编码量以及初次编译时长,增量编译才体现作用。例如:指针的大小为(64位)或32(8位),X发生变化,指针大小却不会改变,文件c.h也不需要重编译。
未完待续
方法还有很多,比如使用多线程,多任务并行编译,分布式编译,预编译等等,另外,在编译大型项目时,分布式编译更优,往往能够大幅度提升性能。