性能优化的目的是为了让程序执行功能变得高效,但同时也不能丧失程序的可维护性和可扩展性。
性能优化是一种实验科学,往往是通过不断迭代进行,在每次优化方案实施完毕后需要对程序的优化前后的性能进行对比来验证优化方案的可行性。
下面主要从C和C++语言入手进行一些代码性能优化上去分析,助力开发相对高性能的软件。
理论基础
影响一个软件程序性能架构的因素主要有两方面分别为:硬件和软件。
影响硬性性能方面的因素有:
- 处理计算机体系结构下存储系统层次结构的排列顺序:
- cpu处理器中允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。
- cpu处理器中的将指令分解为多步,并让不同指令的各步骤重叠,从而几条指令并行处理,以加速程序运行过程的,缩短程序执行时间。
- cpu中允许同时取得多个任务,并同时去执行所取得的的这些任务,并行的效率从代码层次上强依赖于多进程或多线程代码,从硬件角度上更多依赖于多核的cpu,把每一个任务分配给每一个处理器独立完成,在同一时间点,任务一定是同时运行,并行是让不同代码片段同时在不同的物理处理器上执行。
- 并发:
把任务在不同时间点交给处理器进行处理。
在同一时间点,任务并不会同时运行。
- 其他方面:
内存大小、硬盘大小、网络中的网卡、网速。
影响软件性能方面的主要因素有:
- 系统函数调用开销
- 编译器优化
- 语言抽象性
软件的系统函数调用:例如 open、read、fread、write、close、mmap、sbrk、time、gettimeofday等系统函数(因为需要通过系统调用来和内核进行交互)。
编译器优化:在没有同步原语(包括:互斥锁操作、内存屏障、原子操作等等)的情况下,为了程序的性能编译器一般可以在当前线程的结果不变的情况下,自由调整执行顺序。
语言抽象性(表现为词汇级和词法级抽象) : C、C++语言的中间文件是obj文件,它通过在栈上分配了sizeof(obj)字节空间,它们的时间复杂度都是为0(1),相对于C语言C++面向对象中的类机制,涉及到类初始化时候的构造函数调用,类结束时的析构函数,这会给程序带来一定性能影响。
编译器的优化
软件的开发离不开编译器工具作为基础,编译工具的合理利用也可以为程序性能提升提供助推作用。
下面从编译器浅谈下优化的一点点思路。
1.在没有同步原语(互斥锁操作、内存屏障、原子操作)的情况下,编译器为了性能可以在当前线程结果不变的情况下自由调整执行顺序。
2.在编译器中,会自动将语句进行等价转换例如:x=a; y=2; 可以自动转换为 y=2; x=a;再入x=y+1; y=x+2 可等价转换为t=y; y+=3;x=t+1。
3.在编译器中,局部变量可能会被完全消除。
4.全局变量只保证在下一个同步点到来之前写回到内存里。
5.Volatie声明会禁止编译器进行相关的优化。
6.在编译器中,可以使用__attribute__((noinline))防止意外内联。
循环中的优化
程序使用循环语句,在一定情况下会大大增加计算机中CPU的运算时间和效率。因此在程序中的性能优化,循环语句是一个非常大的技术点需要重点设计考虑。
下面针对循环语句罗列几个优化的思路方案。
- 把不必要的反复执行的代码提取到循环外面执行。
- 对于频繁调用的函数考虑使用宏定义替换函数,C++引入inline进行优化,但是有时函数体较长时inline不起作用,所以可以考虑对频繁调用的函数改写为宏定义方式。
- 对一个循环中多个无相关性的处理拆可以将其分成多个循环语句,这样更好的提高cache命中率,在特定场景下可以显著提升性能。
- 减少循环体内的跳转,尽量让流程顺序化执行,从循环中移除不变性代码。
对象参数的优化
如果不修改对象的情况下,建议使用const obj&方式。
如果需要修改对象的情况下,建议使用obj&方式。
如果需要再对象的新拷贝上进行操作的情况下,建议直接使用obj方式。
String接口的优化
- 不推荐使用const String&(除非调用方确保有现成的String对象);
- 如果不需要修改字符串内容,可以使用string_view或const char*;
- 如果只在函数内部修改字符串的内容,可以直接使用String方式;
- 如果需要修改调用者字符串的内容,建议使用string&方式。
函数和虚函数的优化
函数的调用使得处理器跳到另外一个代码地址并回来,这个过程一般需要4个时钟周期,大多数情况处理器会把函数调用、返回和其他指令一起执行以节约运行时间。函数的参数存储在栈上需要额外的时间( 包括栈帧的建立、saving and restoring registers、可能还有异常信息等)。
下面就针对函数相关的罗列一些提高性能的思路。
1.避免过多使用不必要的函数,特别在最底层的循环,应该尽量让代码在一个函数内。看起来与良好的编码习惯冲突(一个函数最好不要超过80行),我们应该知道何时去关注函数的这些优化,而不是一上来就让代码可读性和可为维护性变低。
2.可以使用一些inline函数,让函数调用的地方直接用函数体替换。Inline它对编译器来说是个建议,而且不是inline了性能就好,一般当函数比较小或者只有一个地方调用的时候,inline效果会相对比较好。
3.减少函数的间接调用,如偏向静态链接而不是动态链接,尽量少用或者不用多继承、虚拟继承等风格。
4.优先使用迭代而不是递归。
5.使用函数来替换define,从而避免多次求值。宏的其他缺点:不能overload和限制作用域。
6.减少虚函数的使用,尽可能使用模板方式进行代替虚函数的使用。
7.类的使用,同时在构造函数、析构函数尽可能简单化使用,消除不必要的反复使用构造函数和析构函数。
8.类对象使用时候,复制对象的开销是高昂的。最好选择传递引用,而不是传递值。
运算表达式优化
- 在运行过程中,尽量把常量合并到一起。
例如a*x*b==(a*b)*x
- 当在硬件浮点运算单元的机器上double类型会比float效率高,但一般情况下单精度和双精度的计算性能是一样的。
- 在除法、取余运算情况下,unsigned ints(无符号类型)会快于 signed ints(有符合类型)。
- 除法中,除以常量会比除以变量效率高,因为可以在编译期做优化,尤其是常量可以表示成2^n时。
- ++i和i++本身性能一样,但不同的语境情况下,它们的效果是不一样,如array[i++]比arry[++i]性能好;当依赖自增结果时,++i性能更好,如a=++b,a和b可复用同一个寄存器。
- 浮点除法比乘法慢很多,所以可以利用乘法来代替除法运算,这样可以提高代码性能。
内存优化
程序在运行时,占用内存越少,那么它的运行效率也就更快,也说明程序的运行性能较好。那么如果对这块内存进行做优化,让程序达到更好的性能?
下面分析几种对内存优化的方案。
- 程序尽量减少对内存管理器的调用次数。
- 减少内存读写的操作,特别是减少内存写的次数,并且尽可能按顺序进行内存的访问读取操作。
- 一起使用的函数存储在一起。函数的存储通常按照源码中的顺序来的,如果函数A,B,C是一起调用的,那尽量让ABC的声明也按照这个顺序。
- 一起使用的变量存储在一起。使用结构体、对象来定义变量,并通过局部变量方式来声明,这都是一些较好的选择
- 动态内存分配、STL容器、string都是一些常容易cache不友好的场景,核心代码处尽量不进行使用。
算法优化
在程序开发过程中,可以根据数据集的特征选择更高的数据结构和算法策略,这就要求到开发人员对数据结构和算法空间复杂度和时间复杂度有清晰的认识。
在程序中算法会大大影响程序的性能,因此选择一个合适高效率的算法很重要。
多线程的优化
- 多线程加锁和竞争是影响程序性能的杀手;
- 再多线程中,如果能使用atomic就不要使用mutex;
- 如果读比写多很多,使用读写锁(shared_mutex),而不是使用独占锁(mutex);
- 使用线程本地(thread_local)变量。
总结
程序的性能优化,不仅可以从编译器、语言特性、编码习惯、算法选择、程序架构设计等等方面入手,不断的提升程序的性能,以此达到用户体验感的提升。
最后从项目上梳理几个可以优化的思路点。
1.去除项目中冗余代码。
2.字符串操作优化。
3.减少内存分配、释放操作,例如可以使用内存池。
4.减少不必要的互斥锁操作。
5.根据性能需求选择数据结构。
6.延迟工作,按需执行。
7.减少跨进程的调用。
8.使用高性能的函数库。
9.可以通过使用智能指针代替指针的使用。
10.优化动态库文件的加载,尽量避免不必要的IO操作。
最后推荐一个很不错的在线编码平台(可以将代码自动转换为汇编代码,并且支持多平台汇编代码的转换):COMPILER EXPLORER;
后面是平台链接https://godbolt.org/
本文转载自微信公众号「小道安全」,可以通过以下二维码关注。转载本文请联系小道安全公众号。