C++ 编码优化 | 减少冗余拷贝或赋值 旧城等待, 2022-10-12 01:56 117阅读 0赞 ![1a578cde9b6ef8a178b1a526cbf8d20a.gif][] 置顶/星标公众号????,硬核文章第一时间送达! 链接 | http://www.708luo.com/?p=33 ## 临时变量 ## 目前遇到的一些产生临时变量的情况:函数实参、函数返回值、隐式类型转换、多余的拷贝。 ### 1. 函数实参 ### 这点应该比较容易理解,函数参数,如果是实参传递的话,函数体里的修改并不会影响调用时传入的参数的值。那么函数体里操作的对象肯定是函数调用的过程中产生出来的。 那么这种情况我们该怎么办呢? 如果 `callee` 中确实要修改这个对象,但是 `caller` 又不想 `callee` 的修改影响到原来的值,那么这个临时变量就是必须的了,不需要也没办法避免。 如果 `callee`中根本没有修改这个对象,或者 `callee` 中这个参数本身就是 `const` 型的,那么将实参传递改为引用传递是个不错的选择(如果是基本类型的函数实参,则没有必要改为引用),可以减少一个临时变量而且不会带来任何损失。 另外,推荐一个静态代码检查工具 `cppcheck`,这个工具可以提示非基本类型的 `const` 实参改为引用。 ### 2. 函数返回值(返回对象) ### 函数返回值的情况比较复杂,因为编译器在这方面做了很多优化,编译器优化到何种程度我也没追根究底研究过。 在没开任何优化选项的时候,`gcc` 也优化了一些简单的返回对象的情况。 先看一段代码: A createA(int a) { A tmp; tmp._a=a; return tmp; } 抛开所有优化不谈,函数中 `createA` 应该有一个构造操作(`tmp` 对象生成)和一个拷贝构造操作(`tmp` 对象返回时)。 于是有些编译器尝试对函数返回时的拷贝构造进行优化: A createA(int a) { return A(a); } 第一步可以被优化的拷贝构造就是上面的这种情况,即 `RVO(return value optimization)`,这时候只能在函数返回一个未命名变量的时候进行优化。 后来更进一步,可以在函数返回命名变量的时候也进行优化了,这就是 `NRVO(named return value optimization)`。 但是这时候,还有一种情况不能优化的情况是:如果 `createA`函数内部不同的分支返回不同的对象。 A createA(int a) { if(a%2==0) { A tmp; tmp._a = 2; return tmp; } else { A tmp; tmp._a = 1; return tmp; } } 比如上面这段代码,我在 `gcc 3.4.5` 的情况下测试,发现这种情况是不能优化的。 但是也不排除 `gcc` 更高的版本或者某些在这方面做得更优秀的编译器已经可以优化这种情况。 ### 3. 隐式类型转换 ### 代码中的一些类型的隐式转换也会产生临时变量,比如: class A { public: A(int a=0):_a(a) { cout<<"constructor"<<endl; } A(const A &a) { cout<<"copy constructor"<<endl; this->_a = a._a; } A& operator=(const A&a) { cout<<"operator="<<endl; this->_a = a._a; return *this; } int _a; }; int main() { A a1; a1 = 3; return 0; } 在 a1 = 3 执行时会首先调用 `A(3)` 产生一个临时对象,然后调用`operator=(const A& a)`。 这种情况下,我们只要实现一个`A::operator=(int)`函数就可以避免这个临时对象的产生了。 当然,这只是一个最简单的例子,不过思路是差不多的。 ### 4. 多余的拷贝 ### 这种情况应该比较少,也比较简单,个人感觉,这种情况主要是疏忽引起的。 是这样一种情况: 有个线程级的结构体`thread_data_t *pthread_data`,里面包含请求包的各种数据,在几处使用的使用使用了`const A a = pthread_data->getA()`。 `getA()`的实现简单来说是返回了`thread_data_t`内部的A的成员。 因为在一次请求的处理过程中`thread_data_t`内部的 A 的成员不会改变,调用者用`const A a`来接收这个对象就表明调用者也不会改变返回的 A 成员。 因此,其实完全可以让`getA()`返回A成员的引用,调用者同样用引用来接收:`const A & a = pthread_data->getA()`。 这样就完全就避免了一次多余的拷贝。 ## 非临时变量 ## 遇到的一些非临时变量情况有:`stl vector` 的增长引起拷贝构造、`vector` 的赋值、`std::sort` 操作 ### 1. vector的增长 ### 先简单介绍一下`vector`的增长机制:每次`push_back`时,如果发现原来`vector`的空间用完,会把`vector`调整到原来的 2 倍( sgi 的实现,`visual studio` 的实现好像是 1.5 倍)。因为 `vector` 空间是连续存储的,这里就有一个问题,如果原来 `vector` 地址后面空余的空间没有被使用,那么`vector`继续把后面的地址申请来就可以扩展其空间了。但是如果后面的空间不够了呢?那就要重新申请一个`2*current_size`大小的空间,然后把`vector`当前,也就是`current_size`的内容拷贝到刚申请的那块空间中去,这时就引起了对象的拷贝操作了。 假设`vector`初始大小是 0,我们通过`push_back`加入了 10 个对象,以`sgi`实现的两倍增长为例,再假设每次调整`vector`空间的时候都需要调整地址,一共引入了多少次无用的拷贝? 因为`vector`空间是`1->2->4->8->16`增长的,拷贝的次数一共是四次,每次拷贝对象分别是`1、2、4、8`个。所以答案是`1+2+4+8=15`。 很容易看出规律,拷贝对象的个数等于最终`vector`空间大小减一。 那么如果`vector`大小最终会涨到 1000,1W 呢?数据就很可观了。 我接触过好几个服务,最终`vector`可能会增长到 10W 左右的。如果`vector`要放入 10W 个元素,那么就会开辟`131072`的空间,也就是说最多会引入 13W 次的对象拷贝,而这个拷贝操作是无效的、是可以避免的。 其实要避免`vector`增长引入的拷贝也很简单,在`push_back`之前先调用`reserve`申请一个估算的最大空间。 比如我们之前优化的一些服务,预期`vector`最大可能会增长到 10W,那么直接调用`v.reserve(100000)`就可以了。 当然,这也许会引起一些内存使用的浪费,这就需要使用时注意权衡了。 但如果你的服务是一直运行的,而且这个`vecto`r对象也是常驻内存的,个人觉得完全可以`reserve`一个最大的空间。因为`vector`空间增长之后,就算调用`clear`清除所有元素,内存也是不会释放的。除非使用和空`vector`交换的方式强制释放它的内存。 ### 2. vector的赋值 ### 遇到过这样一种情况,在一个函数接受一个`vector &`作为输入,经过一系列处理得到一个临时的`vector`,并在函数返回前将这个临时的`vector`赋值给作为参数的`vector &`作为返回值。简化一下代码如下: void cal_result(vector<int> &input_ret) { vector<int> tmp; for(...) { ... // input_ret will be used //fill tmp } input_ret = tmp; } 这里,我们可以注意到函数返回后 `tmp` 对象也就消失了,不会被继续使用,所以如果可以的话,我们根本不需要返回 `tmp`的拷贝,直接返回 `tmp` 占用的空间就可以了。 那么怎么可以直接返回 `tmp` 而不引起拷贝呢?是不是可以这样想,我们把 `tmp`这个`vector`指向的地址赋值给`input_ret`,把`tmp`指向的空间和大小设置为 0 就可以了? 那么我们完全可以使用`vector`的`swap`操作。它只是将两个`vector`指向空间等等信息交换了一下,而不会引起元素的拷贝,它的操作是常数级的,和交互对象中元素数目无关。 因此将上述代码改为: void cal_result(vector<int> &input_ret) { vector<int> tmp; for(...) { ... // input_ret will be used //fill tmp } input_ret.swap(tmp); } 可以减少`tmp`元素的拷贝操作,大大提高了该函数的处理效率。(提高多少,要看`tmp`中所有元素拷贝的代价多大) ### 3. std::sort操作 ### 在为一个模块做性能优化的时候,发现一个`vector`的`sort`的操作十分消耗性能,占了整个模块消耗`CPU 10%`以上。 使用`gperftools`的`cpu profiler`分析了一下数据,发现`sort`操作调用了元素的拷贝构造和赋值函数,这才是消耗性能的原因。 进一步分析,`vector`中的元素对象特别庞大,对象中又嵌套了其他对象且嵌套了好几层,因此函数的拷贝和赋值的操作代价会比较大。 而`std::sort`采用的是内省排序+插入排序的方式( sgi 的实现),不可避免地会引入对象的交换和移动。(其实不管怎么排序都避免不了交换和移动的吧...) 因此,要优化这句`std::sort`操作,还需要减少对象交换或者提高交换的效率上入手。 1. 减少对象的交换 我们采用的减少对象交换的方式是:先使用`index`的方式进行排序,排序好了之后,把原来的`vector`中的对象按照`index`排序的结果最终做一次拷贝,拷贝到这个对象排序后应该在的位置。 1. 提高交换的效率 如果对象的实现是如下这样的: class A { public: A(const char* src) { _len = strlen(src); _content = new char[_len]; memcpy(_content,src,_len); } A(const A &a) { *this = a; } A& operator=(const A&a) { _len = a._len; _content = new char[_len]; memcpy(_content,src,_len); } private: char *_content; int _len; }; 这里为了保持代码简短,省略了部分实现且没考虑一些安全性的校验。 那么在对象交换的时候,其实是没有必要调用拷贝构造函数和赋值函数的(`std::swap`的默认实现),直接交换两个对象的`_content`和`_len`值就好了。如果调用拷贝构造函数和赋值函数的话,不可避免还要引入`new、memcpy、strlen、delete`等等操作。 这种情况下,我们完全可以针对 A 的实现,重载全局的`swap`操作。这样`sort`的过程中就可以调用我们自己实现的高效的`swap`了。 如下代码可以重载我们 A 函数的`swap`实现: namespace std { template<> void swap<A>(A &a1,A& a2) { cout<<"swap A"<<endl; int tmp = a1._a; a1._a = a2._a; a2._a = tmp; } } 因为调用堆精度问题和编译优化的问题,有时候也可能分析不到 `sort` 是因为调用了元素对象的拷贝构造和赋值函数所以才效率比较低。所以发现`sort`消耗性能的时候,可以看看是否是因为`sort`对象过大造成的,积累一个`common sense`吧。 **往期****推荐** ☞ [专辑 | 趣味设计模式][Link 1]![172981aa2272ab1fc55ecee641faee11.gif][] ☞ [专辑 | 音视频开发][Link 2] ☞ [专辑 | C++ 进阶][_ C]![919d5894c6b02ad06824a5597e52b457.gif][] ☞ [专辑 | 超硬核 Qt][_ _ Qt]![87213f6bb27fc4e4b0fcbc81aceaaa52.gif][] ☞ [专辑 | 玩转 Linux][_ _ Linux] ☞ [专辑 | GitHub 开源推荐][_ GitHub]![6d3d6faf769136fff64604b86e9b9006.gif][] ☞ [专辑 | 程序人生][Link 3] **关注****公众****号****「高效程序员」**????,一起优秀! 回复“1024”,送你一份程序员大礼包。 [1a578cde9b6ef8a178b1a526cbf8d20a.gif]: /images/20221005/648e5536fb9a4a5eb095b68c1a002872.png [Link 1]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1511180677587795970#wechat_redirect [172981aa2272ab1fc55ecee641faee11.gif]: /images/20221005/369f10174e904e2a806fb869e00aa01a.png [Link 2]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1353104804985389056#wechat_redirect [_ C]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1511180677537464321#wechat_redirect [919d5894c6b02ad06824a5597e52b457.gif]: /images/20221005/95be106283b24aef8740944c698d844b.png [_ _ Qt]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1514085447671611393#wechat_redirect [87213f6bb27fc4e4b0fcbc81aceaaa52.gif]: /images/20221005/9ad42f95b08945d49f626be765663aaf.png [_ _ Linux]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1571518469139988480#wechat_redirect [_ GitHub]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1634317960456568833#wechat_redirect [6d3d6faf769136fff64604b86e9b9006.gif]: /images/20221005/1d1b8642adfd4de893ecbfe507d43d5f.png [Link 3]: https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzA4MjI3NzQ1Nw%3D%3D&action=getalbum&album_id=1683541658728005634#wechat_redirect
相关 代码优化挑战:减少Java代码冗余实例 在Java编程中,冗余代码通常意味着重复的工作或者逻辑。优化的目标是消除这些重复,提高代码的可读性和维护性。以下是一些减少Java代码冗余的例子: 1. **使用方法和变量* 小灰灰/ 2024年09月10日 09:24/ 0 赞/ 16 阅读
相关 赋值、切片、拷贝 参考:[Python赋值、切片、拷贝解析][Python] 赋值不可变对象要看是否有缓存机制来决定是否是同一对象 赋值可变对象相当于引用,完全不拷贝 切片相当于浅拷贝 青旅半醒/ 2022年12月15日 11:07/ 0 赞/ 126 阅读
相关 C++ 编码优化 | 减少冗余拷贝或赋值 ![1a578cde9b6ef8a178b1a526cbf8d20a.gif][] 置顶/星标公众号????,硬核文章第一时间送达! 链接 | http://www.708 旧城等待,/ 2022年10月12日 01:56/ 0 赞/ 118 阅读
相关 C++编码优化之减少冗余拷贝或赋值 临时变量 目前遇到的一些产生临时变量的情况:函数实参、函数返回值、隐式类型转换、多余的拷贝。 1. 函数实参 这点应该比较容易理解,函数参数,如果是实参传递的话, 淩亂°似流年/ 2022年10月11日 13:57/ 0 赞/ 130 阅读
相关 C++ 拷贝构造函数 赋值构造函数 C++ 拷贝构造函数 赋值构造函数 拷贝构造函数和赋值构造函数的异同 由于并非所有的对象都会使用拷贝构造函数和赋值函数,程序员可能对这两个函数有些轻视。请先记住以下的警告 r囧r小猫/ 2022年09月18日 04:51/ 0 赞/ 247 阅读
相关 C++ 拷贝构造函数和赋值运算符 转:[https://www.cnblogs.com/wangguchangqing/p/6141743.html][https_www.cnblogs.com_wangguc 你的名字/ 2022年06月05日 03:22/ 0 赞/ 196 阅读
相关 C++常见问题总结_拷贝控制(拷贝、赋值、销毁) 当我们定义一个类时,我们显示或隐式地指定在此类型对象拷贝、赋值和销毁时做什么。 一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造 £神魔★判官ぃ/ 2022年05月23日 09:56/ 0 赞/ 237 阅读
相关 c++:类拷贝控制 - 拷贝构造函数 & 拷贝赋值运算符 一、拷贝控制 当定义一个类时,我们可以显式或隐式地指定此类型的对象拷贝、移动、赋值和销毁时做什么。 一个类可以通过定义五种特殊的成员函数来控制这些操作,包括:++拷贝构 怼烎@/ 2022年05月14日 02:24/ 0 赞/ 316 阅读
相关 减少代码冗余 机房 一:防止往下拉菜单输入: Private Sub comboRelation2_KeyPress(KeyAscii As Integer) '防止输入字符 妖狐艹你老母/ 2022年05月09日 05:33/ 0 赞/ 214 阅读
还没有评论,来说两句吧...