写出高效代码的12条建议

大家好,我是程序喵!

图片

今天和大家介绍一下能让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工具,找到瓶颈所在,然后做相应优化。

另外产品和测试如果没给你提性能需求,那优化它干嘛!

图片

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序喵大人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值