我们将从内存和效率两个方面宏观的提出一些代码优化的可行建议。代码优化如刀尖跳舞,需要对每一个收益进行预测和计
没有0成本的收益
——C++设计哲学
我们应该从哪些方面对代码进行优化
选择合适的算法
无痛优化(直接查找与二分查找)
直接查找的时间复杂度为O(n*n),二分查找的时间复杂度为O(logn),也就是说只要选择了正确的算法可以很明显对效率进行压缩,同时保持空间复杂度不变。
内存宽松但是时间有限的选择(哈希表)
哈希表的查找时间复杂度为O(1)但是哈希表是一种明显的时间换空间的查找方法,也就是说在内存空间不受限的情况下哈希表是一个很好的选择。
内存有限但时间宽松的选择(分块查找)
分块查找的好处是尽可能少的空间进入内存,分块在对应的区域查找,时间复杂度一般为O(N*N)(随着在内存中查找算法的变化而变化),即可以尽可能的节省内存空间。
对算法的评价
算法的评价标准不只是代码的时间复杂度和空间复杂度这么简单,如果仅仅用时间复杂度空间复杂度进行评价将会出现比较大的偏差。
时间复杂度的欺骗性
算法的时间复杂度一般指的是算法在构建完毕以后执行的最坏时间,但是我们有可能遇到以下几种可能
1.一个完全无序的数组,要做查找,如果使用二分查找,第一步是对数据进行排序,第二步才是对数据进行查找。
2.对于数据,大概率需要查找的数据会出现在头部,那么在这种数据特征的情况下,顺序查找反而是最快的查找算法。
空间复杂度的危险性
空间复杂度本身不具有很多的欺骗性,即对Java来说相对安全,但是对于C++这种多范式语言来说如果认为堆栈空间是等同的有很大的威胁。
我们来看下面的两个函数分别是C++和Java写的递归
//C++递归
class memory
{
public:
int a[10]={0};
}
static int count=0;
void fun()
{
memory data;
if(count<=1000)
{
count++;
fun();
}
}
int main()
{
fun();
}
//class one
public class memory
{
public int a[10];
memory(){}
}
//class two
import memory
public class MAIN
{
public int count=0;
public static void fun()
{
memory middle=new memory();
if(count<10000)
{
cout++;
fun();
}
public static void main(String args[])
{
fun();
}
}
C++的递归是在栈上构建相应的数据,而Java的递归是在堆上构建相应的数据,两者的时间复杂度和空间复杂度完全相同,但是两者的危险性完全不同,在栈上构建数据的好处在于由编译器自动管理空间,在生命周期结束后释放,但是问题就在于栈的空间是有限的,相对于堆空间来说一般小很多,大量的数据在栈上构建的结果就是数据量超越栈大小,操作系统直接进入异常处理,将整个进程终止。而在堆上构建失败的结果无非返回一个空指针(Java的引用本质上也就是C++的指针)。这也就是算法构建中内存的危险性。
编译器
用更好的编译器
目前市面上主流的C++编译器有gcc,MSVC,clang,InterC++,从实践角度看,gcc是所有编译器中最稳定也是对新特性支持最好的,但是速度居中,MSVC只能在windows上使用,对新特性的支持中等,速度中等,clang的对新特性支持较差,速度比较快,InterC++使用了最为激进的优化策略,生成的代码速度最快,但是也最有可能出现未知的运行结果,需要程序员对适当的地方打上禁止优化标识。
用好编译器
对同一份代码,使用不同的编译选项最后得出的结果是完全不同的,release的大小会小于debuge,O1,O2,O3优化逐渐激进,速度逐渐加快,但是默认情况下的编译命令,编译器不会打开优化,对编译器的正确使用是代码优化中很重要的一环。
用更好的库
C++中有大量的库每一种库的实现都是基于不同的想法,很多算法在STL中都做了实现,但是每个程序员都要有一个清晰的认知,官方库是问题的解决方案但是不是问题的最佳解决方案。官方库中对每个实现都做了多方面的努力,即可以认为官方库是最完备的。但是世界是复杂的,我们要考虑这些特性是否是我们需要的,如异常安全,我们需要嘛,抛出异常的概率有多大,如果这个算法只在运行的开头用一次,其他地方不使用,拿我们真的需要异常安全嘛,在开始运行时出现异常终止程序反而是比价好的解决方案。所有的特性保证都是需要额外的代码进行支撑的,也就是需要计算时间和内存空间进行保证的,没有无成本的收益在软件工程领域是永远不褪色的。那在这种情况下我们就需要选择速度更快的库或者自己对某些库进行改写来获得更高的效率。
尽可能少的内存分配和复制
内存的分配和复制是要大量的计算时间进行保证的,尤其是对于C++这种直接向内存申请空间,多种范式传值的语言来说。
Java需要在对JVM进行调优,即一次性分配足够大的内存空间,而不是JVM一次一次的向操作系统申请。
善用语言特性
例如C++的一个很重要的优化方式在于大量的计算可以在编译期完成,即C++新特性中引入的编译期计算,在很大层度上可以对运行时间进行节省。
C++中还引入了标准的标识方式用于和编译器交流,如[likely]可以控制指令流水走向特定的分支,可以由程序员完成对不可预测位置的指令流水的控制。
使用更好的数据结构与数据存储方式
在C++中在不影响可读性的情况下尽量少用class以及struct封装,与C语言不同这两者的封装会有不一样的成本,C++中的结构体的构建会有构造函数和析构函数的调用,需要有额外的语句来完成。
同样在java中八大数据类型能用数据类型本身尽量不要使用对应的封装类,封装类的构建装箱开箱都需要有特定的指令支持(由编译器自动插入)。