查看原文
其他

写出高效代码的12条建议

程序喵大人 程序喵大人 2022-08-22

大家好,我是程序喵,最近迷上了云宗主,所以放了个云韵的封面,下次估计会是美杜莎!



今天和大家介绍一下能让C++代码更加高效的几个小技巧,话不多说,以下为本文目录:

  • 参数传递方式:值传递还是引用传递

  • 函数返回方式:按值返回还是按引用返回

  • 使用移动语义

  • 避免创建临时对象

  • 了解返回值优化

  • 考虑预分配内存

  • 考虑内联

  • 迭代 vs 递归

  • 选择高效的算法

  • 利用缓存

  • profiling

  • other碎碎念


以下为正文:


值传递还是引用传递:

一般情况下使用const的引用参数。对于函数本身会拷贝的参数,最好使用值传递,但只有当参数的类型支持移动语义时才这样。


在某些情况下,值传递并移动实际上是向函数传递参数的最佳方式(注意看后面的tips),例如:

class A {   public:    A(const std::string &str) { str_ = str; }
   private:    std::string str_;};


可以考虑改为这种形式:

class A {   public:    A(std::string str) { str_ = std::move(str); }
   private:    std::string str_;};


因为无论如何都会对它们进行拷贝。


tips:看有些资料说后者值传递是更好的参数传递方式,貌似有些道理,但是我没找到非常合理的理由,有知道的读者可以在评论区留言。


按值返回还是按引用返回:

可以通过从函数中按引用方式返回对象,以避免对象发生不必要的复制。但有时不可能通过引用返回对象,例如编写重载的operator+和其他类似运算符时。


永远都不要返回指向局部对象的引用或指针,局部对象会在函数退出时被销毁。


但是,按值返回对象通常没啥大问题。因为一般情况下他们会触发返回值优化或移动语义,即不会有多余的拷贝动作。


使用移动语义:

尽量确保对象拥有移动构造函数和移动赋值运算符。对象有了移动语义后,许多操作都会更加高效,特别是与标准库和算法相结合时。


避免创建临时对象:

没有必要的临时对象能避免就避免。一般来说,应该避免迫使编译器构造临时对象的情况。尽管有时这是不可避免的,但是至少应该意识到这项“特性”的存在,这样才不会为实际性能和分析结果而感到惊讶。编译器还会使用移动语义使临时对象的效率更高。这是要在类中添加移动语义的另一个原因。


《More Effective C++》第19条款中介绍过:所谓的临时对象并不是程序员创建的用于存储临时值的对象,而是指编译器层面上的临时对象:这种临时对象不是由程序员创建,而是由编译器为了实现某些功能(例如函数返回,类型转换等)而创建。


比如下面的代码就会有临时对象的产生:

void Func(const std::string& s);char arr[]="hello";Func(aar); // here


返回值优化

通过值返回对象的函数可能导致创建一个临时对象。看下面的代码:

Person createPerson(){ Person newP { "Marc", "Gregoire", 42 }; return newP;}


假如像这样调用这个函数(假设Person 类已经实现了operator<<运算符):

cout << createPerson();


即便这个调用没有将createPerson()的结果保存在任何地方,也必须将结果保存在某个地方,才能传递给operator<<。为此编译器创建一个临时变量,来保存createPerson()返回的Person 对象。


即使这个函数的结果没有在任何地方使用,编译器也仍然可能会生成创建临时对象的代码:

createPerson();

编译器可能生成代码来创建一个临时对象来保存返回值,即使这个返回值没有使用也是如此。


不过吧,编译器会在大多数情况下优化掉临时变量,以避免复制和移动。


关于返回值优化我之前有篇文章介绍过,感兴趣的可以看看这个:《左值引用、右值引用、移动语义、完美转发,你知道的不知道的都在这里


预分配内存:

比如标准化容器中的reserve,需要频繁创建内存的地方可以考虑预分配一块内存出来,避免频繁的创建内存。


内联函数:

短函数可以使用内联消除函数开销。


迭代 vs 递归:

这里我更倾向于选择迭代方式,而不是递归。递归占用大量栈内存,且可能会产生很多不必要的临时对象构建。


选择效率更高的算法:

学计算机的估计没有不知道算法的吧,学算法估计没有人不知道如何计算时间复杂度和空间复杂度吧,在平时开发过程中遇到算法问题时我们可尽量选择效率更高的算法,比如O(N)和O(N2)的算法,我们肯定要选择O(N)的呀,这里可以了解下C++的,这里引入了很多高效的算法供我们使用。


尽可能多的使用缓存:

将某些数据保存下来供下次使用,避免再次获取或重新计算它们。如果任务或计算特别慢,应该保证不执行那些没有必要的任务或者重复计算。


  • 网络通信:如果频繁发起相同的网络请求,可考虑将第一次的网络请求结果保存在内存中,或文件中?

  • 磁盘访问:如果频繁访问一个文件,可考虑将这个文件的内容保存在内存中。

  • 数学计算:某些很耗时很复杂的运算,可考虑只执行这种计算一次,然后共享结果。

  • 对象分配:如果需要大量频繁创建和销毁短期对象,可考虑使用对象池。

  • 线程创建:如果需要大量频繁创建和销毁线程,可考虑使用线程池。


做客户端开发的朋友应该都听说多级缓存的概念,就是这个原理。


profiling:

性能问题永远离不开profiling工具,多用profiling工具。工具篇我之前介绍过:《这么多性能调优工具,看看你知道几个?


other碎碎念:

  • 选择合适的数据结构:

    选择合适的STL,想清楚什么时候用栈,什么时候用队列,什么时候用数组,什么时候用链表。

  • 某些if-else可改为switch,效率可能更高(知道为什么吗,不知道的可以留言,人多的话考虑输出一篇文章)。

  • 优先考虑栈内存,而不是堆内存(免得频繁的申请释放内存)。

  • 如何函数不需要返回值,就不要设置返回值。

  • 使用位操作,移位代替乘法除法操作。

  • 构造函数时使用初始化方式,而不是赋值。

A::A() : a_(a) {} // better
A::A() { a_ = a;}
  • 明确使用模板带来的益处:

    如果使用模板并没有给你的开发带来任何益处,是不是可以考虑不使用它,因为调试起来真的麻烦。

  • 函数参数的个数不要太多。

  • 擅用emplace,有些情况下会省去一次构造的开销。


最后想说一句:

先完成再完美。
不要一开始就想着写最完美的代码,很多bug都是过早过度优化导致的。
一般情况下,性能较高的代码可读性都不是特别高。提早优化很可能引发很多bug。
很多情况下,我们可以先完成代码,确保功能完成且正确之后,再去考虑完善。
完成代码后,可以使用profiling工具,找到瓶颈所在,然后做相应优化。
另外产品和测试如果没给你提性能需求,那优化它干嘛!




往期推荐



高端 | 如何快速定位程序Core?

推荐几个开源库

C 语言的那些坑!

高端知识点 — socket fd 是什么?

百家号在线视频编辑器的技术演进

Linux C++ 服务器端这条线怎么走?

关于多线程,我给出13点建议


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存