写出高效代码的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,有些情况下会省去一次构造的开销。
最后想说一句:
往期推荐