个人觉得虽然没必要过度优化,现代编译器在优化方面已经做得比较好了,但是在有些方面,还是需要程序员主动去做的。
不过度优化≠不主动优化
深度学习跑模型的时候,有时候可能一跑就是几天,哪怕只对程序优化10%,效果也是非常可观的。在进行优化的时候,程序可能因此可读性和可维护性都变差,这就需要程序员自己去权衡了,在速度非常重要的时候,做一些牺牲也是可以接受的。
1.高级设计。为遇到的问题选择适当的算法和数据结构。要特别警觉,避免使用那些会渐进产生糟糕性能的算法和编码技术。
2.基本编码原则。避免限制优化的因素,这样编译器就能产生高效的代码。
这点是非常重要的,因为其他优化编译器都可以代替你做,但是以下两点,编译器是没办法做优化的,至于为什么这两点会导致编译器没办法优化,这里不再赘述。
1)消除连续的函数调用。在可能时,将计算移到循环外。考虑有选择性的妥协程序的模块性已获得更大的效率。
除此之外,在可能的情况下,使用内联函数也是一个很好的选择。
举个例子:
在C++11以前,容器成员函数size()都是O(n),如果采用以下写法,原本O(n)复杂度的代码就变成了O(n^2)。
for (int i=0;i<str.size();i++)
正确的做法是尽量将函数调用提到循环外。
2)消除不必要的存储器引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果放到数组或全局变量中。
这个应该很好理解,学过计算机组成原理的都知道存储器速度远低于寄存器速度。
3.低级优化
1)展开循环,降低开销,并且使得进一步的优化成为可能。
现代编译器一般都会进行循环展开优化,但是碰到严重限制程序速度的代码,还是可以尝试用循环展开改进速度。
2)通过使用例如累积变量和重新结合等技术,找到方法提高指令级并行。
①累计变量
例如a1*a2*a3*a4
可以改成求(a1*a2)*(a3*a4)
如果用分治法,可以把原本O(n)复杂度减少到O(logn)
②重新结合
那么重新结合是意思呢,看以下代码:
int a=0,b=1,c=3;
第一种写法: a=a+b+c;
第二种写法: a=a+(b+c);
乍一看这两种写法没什么区别,但是实际上两种代码的关键路径是不一样的。在汇编代码中,关键路径限制代码执行代码执行速度。
第一种写法,应该先将a+b保存在寄存器c中,即存在寄存器写后读相关,指令不能并行执行。但是第二种写法,b+c保存在不同寄存器中,两条乘法指令互相不影响。
以前学计算机组成原理的时候,这种冲突可以通过记分牌算法和Tomasulo算法解决,但是让硬件去优化,还是会增加开销。我用clang试了一下,编译器也是会做此类优化的。所以一般情况下,这类优化并没有太大必要。
3)使用功能的风格重写条件操作,使得编译采用条件数据传送。
C++中在可能的情况下,用三目运算符 ?:代替条件语句。(条件传送指令更快)