最好的朋友:C++11移动语义和Pimpl手法

当编译器可以用廉价的挪动操作替换昂贵的复制操作时,也就是当它可以用一个指向一个大对象的指针的浅层复制来替换对这个大对象的深层复制的时候,挪动语义要比复制语义更快速。因此,在类中利用
PIMPL方法 结合挪动语义,应该能预见到有相当大的速度提升。由于QT对于每个非常规类都采用PIMPL方法,因此通过简单地使用Qt类而不是与它们对应的STL,我们应该可以看到速度有很大的提升。我将会对使用了挪动语义,应用和没有应用PIMPL方法的Qt和STL类进行比较。

使用了挪动语义和 PIMPL 方法的一个类

在我的文章《通过 C++11 挪动语义提升性能》中我们将PIMPL方法应用到了CTeam这个类。


  1. // cteam.h 
  2.   
  3. #ifndef CTEAM_H 
  4. #define CTEAM_H 
  5.   
  6. #include <memory> 
  7.   
  8. class CTeam 
  9. public: 
  10.     ~CTeam();                                      // dtor 
  11.     CTeam();                                       // default ctor 
  12.     CTeam(const std::string &n, int p, int gd);    // name ctor 
  13.   
  14.     CTeam(const CTeam &t);                         // copy ctor 
  15.     CTeam &operator=(const CTeam &t);              // copy assign 
  16.   
  17.     CTeam(CTeam &&t);                              // move ctor 
  18.     CTeam &operator=(CTeam &&t);                   // move assign 
  19.   
  20.     std::string name() const; 
  21.     int points() const; 
  22.     int goalDifference() const; 
  23.   
  24. private: 
  25.     struct Impl; 
  26.     std::unique_ptr<Impl> m_impl; 
  27. }; 
  28.   
  29. #endif // CTEAM_H 

CTeam 的公共接口跟之前一样。我们将私有数据成员用一个独立的指针替换并将它们移动到是有实现类 CTeam::Impl
中去。CTeam::Impl 的声明和定义被放在了源代码文件 cteam.cpp 中。这就是 pimpl 方法巨大的优势之一:
头文件不包含任何实现细节。因此我们就能够修改pimpl方法化的类而不用修改接口(见Marc Mutz的文章《Pimpl方法Pimpl化》以了解更多pimpl方法的优势。


  1. // cteam.cpp 
  2. #include ... 
  3.   
  4. using namespace std; 
  5.   
  6. struct CTeam::Impl 
  7.     ~Impl() = default; 
  8.     Impl(const std::string &n, int p, int gd); 
  9.     Impl(const Impl &t) = default; 
  10.     Impl &operator=(const Impl &t) = default; 
  11.   
  12.     std::string m_name; 
  13.     int m_points; 
  14.     int m_goalDifference; 
  15.     static constexpr int statisticsSize = 100; 
  16.     std::vector m_statistics; 
  17. }; 
  18.   
  19.   
  20. CTeam::Impl::Impl(const std::string &n, int p, int gd) 
  21.     : m_name(n) 
  22.     , m_points(p) 
  23.     , m_goalDifference(gd) 
  24.     m_statistics.reserve(statisticsSize); 
  25.     srand(p); 
  26.     for (int i = 0; i < statisticsSize; ++i) { 
  27.         m_statistics[i] = static_cast(rand() % 10000) / 100.0; 
  28.     } 

注意 C++11 关键词 default如何帮助我们节省实现析构器、复制构造器以及复制实现类 CTeam::Impl 这些琐碎的代码。我们必须只为特殊命名的构造器编写代码。剩下来的由编译器来生成。

我们将使用CTeam::Impl来实现面向使用者的CTeam类的构造函数与赋值运算符:


  1. // cteam.cpp (续) 
  2.   
  3. CTeam::~CTeam() = default; 
  4.   
  5. CTeam::CTeam() : CTeam("", 0, 0) {} 
  6.   
  7. CTeam::CTeam(const std::string &n, int p, int gd) 
  8.     : m_impl(new Impl(n, p, gd)) 
  9. {} 
  10.   
  11. CTeam::CTeam(const CTeam &t) 
  12.     : m_impl(new Impl(*t.m_impl)) 
  13. {} 
  14.   
  15. CTeam &CTeam::operator=(const CTeam &t) 
  16.     *m_impl = *t.m_impl; 
  17.     return *this; 
  18.   
  19. CTeam::CTeam(CTeam &&t) = default; 
  20.   
  21. CTeam &CTeam::operator=(CTeam &&t) = default; 
  22.   
  23. std::string CTeam::name() const 
  24.     return m_impl ? m_impl->m_name : ""; 

我们让编译器自己来合成析构函数。默认构造函数委派了具名构造函数。具名构造函数以给定的参数创建了一个Team::Impl的对象,一切符合我们所预想的情况。

复制构造函数和复制赋值运算符必须进行深拷贝。而编译器自动合成的版本只会复制m_impl这个指针,也就是说进行的是浅拷贝。因为这是错误的,我们必须手动书写自己的复制构造函数与赋值运算符,这段代码调用了实现类(Impl)的复制构造函数和赋值运算符。

移动构造函数与移动赋值运算符只需要浅拷贝就行了。默认的实现就复制了m_impl指针(浅拷贝),然后将被移动的对象中的m_impl置为nullptr。移动操作将Impl对象的所有权从被移动的位置转移到了目的地的CTeam对象之中。这项行为是由std::unique_ptr实现的,其仅支持移动而不支持拷贝。

因为m_impl这个指向实现类的指针可以为空,例如CTeam::name之类的函数应该在使用这个指针前检查合法性。

基准

我们使用ShuffleAndSort(混乱并排序)和尾部压入为测试基准。我们不使用EmplaceBack作为测试基准,因为Qt5.7(写这篇文章时候的最新版本QT)不支持在Qt容器中使用emplace操作。如此处所示: 通过C++11的移动语义带来性能提升

我运行了不同的实验,我用以下标签标记。

  • C++98 – 用C++98编译器内置的示例代码
  • C++11 – 用C++11编译器内置的示例代码
  • Copy – 类CTeam只有拷贝构造,但没有重载移动构造
  • Move – 类CTeam同时具有拷贝和移动构造
  • STL – 使用std::string和std::vector在示例代码中
  • Qt – 使用QString和QVector在示例代码中
  • Pimpl – 使用pimpl 手法在类CTeam中
  • Opt – 使用lambdas去排序,并使用C++11的随机数生成器

我们每次实验通过callgrind 计数读取指令的个数进行性能测定。由于相对性能比的读取指令绝对数值更能说明问题,所以我们把C++11/Moves的测试结果转为1.000来参考。

以下是测试结果。

                实验                 ShuffleAndSort                 PushBack
                C++98/STL/Copy                 1.693                 1.006
                C++98/Qt/Copy                 1.335                 1.048
C++11/STL/Move 1.000 1.000
                C++11/Qt/Move                 0.773                 1.049
                C++11/STL/Move/Pimpl                 0.730                 1.011
                C++11/Qt/Move/Pimpl                 0.724                 1.071
                C++11/STL/Move/Pimpl/Opt                 0.597                 0.308
                C++11/Qt/Move/Pimpl/Opt                 0.589                 0.399
                C++11/STL/Move/Opt                 0.867                 0.296
                C++11/Qt/Move/Opt                 0.638                 0.378

在`ShuffleAndSort`基准方面,QT的实验结果(绿色部分)比STL的实验结果(红色部分)要快(大概快1.01-1.36倍)。原因很简单。QT在像`QVector`和`QString`这样的重要的类中都使用了PIMPL手法。拷贝一个QT的隐式共享类意味着仅拷贝指针。使用PIMPL的类执行的是浅拷贝而不是深拷贝。在这种情况下,移动主义的性能比复制语义的快。

但是使用PILPL手法本身也是需要开销的。当我们使用PIMPL创建一个对象时,我们创建了一个“接口”对象(比如CTeam),“接口”对象再从堆中动态地返回一个“实现”对象(例如CTeam::Impl)。这就是为什么在`PushBack`基准方面,QT的实验结果普遍比STL的实验结果慢(大约慢1.04-1.30倍)。在任何时候调用一个自定义的构造函数(例如构造函数CTeam())、拷贝构造函数或拷贝赋值操作符以及所有需要执行深拷贝的地方,PIMPL都会产生一定的花销。

如果仅考察STL实验也会得到类似的结论。在ShuffleAndSort,使用PIMPL手法的STL实验,其运行结果比没有使用PIMPL的要快。在PushBack,情况则相反。使用PIMPL手法的STL实验,其运行结果比没有使用PIMPL的要慢。

ShuffleAndSort是使用移动语义和PIMPL的最佳案例。它先执行20遍拷贝操作来填teams容器。然后在打乱和排序的过程中执行810,000次移动。同样地,PushBack是使用移动语义反面代表。它调用CTeam的同名构造函数、复制构造函数、析构函数100,000次。在调用同名构造函数的过程中,需要从堆中动态地创建实现对象,这很明显需要时间开销。

当我们比较使用Pimpl与那些不使用pimpl实验(C++11/STL/Move vs.
C++11/STL/Move/Pimpl,C++11/STL/Move/Opt vs.
C++11/STL/Move/Pimpl/Opt),我们看到的是一个加速因子1.370到1.452
和缓慢的因素1.011到1.041。使用移动语义和Pimpl增速的幅度比慢造成的pimpl开销命令。如果我们的代码更倾向于shuffleandsort,其中浅拷贝支配深拷贝,我们的代码将最有可能从与Pimpl惯用法组合使用移动语义看全面提速。

幸运的是,在大多数情况下,在真正的代码中,浅副本占主导地位的深层副本。这个观察是必要的当QT项目决定在一开始使用Pimpl惯用法的所有非平凡的类。

如果我们比较使用STL和QT实验之间的Pimpl惯用法的开销(C++11/STL/Move/Pimpl vs.
C++11/Qt/Move/Pimpl,C++11/STL/Move/Pimpl/Opt vs.
C++11/Qt/Move/Pimpl/Opt)以下的图片出现。为阻挠,QT比STL慢1.06到1.30倍。原因是,cteam纯Qt版本使用pimpl的字符串m_name和双打m_statistics矢量。ShuffleAndSort,QT只是稍微快点(因子:1.008–1.014)比纯C++
11 / STL。这个小的增速很可能是吃了更大的减速的pimpl开销造成的。

在前C + +的11倍,使用Qt的课给了我们一个速度的优势超过STL类大多数时候。事情有C++
11的到来改变了。STL类现在看齐的Qt类–感谢移动语义的组合和Pimpl惯用法。纯C++
11实施给我们更好地控制何时使用Pimpl惯用法时。用QT,我们一直用它–无论产量增速与否。

结论

移动语义给了我们一个加速复制的语义,编译器可以通过移动操作取代昂贵的复制操作。所以,结合移动语义和 pimpl idiom
应该是非常适合的,随着 pimpl idiom
代替昂贵的深拷贝大对象,代价更低的浅拷贝对象的指针直接指向这些大对象。我们的研究结果也证实了这一现象。我们可以看到加速系数是2.319,它是以
ShuffleAndSort 为基准的移动语义和 pimpl idiom 。使用 pimpl idiom
也不是免费的,因为我们必须动态地创建指向对象的指针,这在堆上会有一个额外的步骤。PushBack 基准显示使用 pimpl
会放慢1.005倍的速度。

ShuffleAndSort 基准是一种最好的 pimpl idiom ,因为几乎所有的操作都是移动操作(洗牌并排序)。PushBack
基准则是相反的,它明显倾向于 ShuffleAndSort
的极致。针对这类情况,我们会看到一个速度的提升,因为速度的提升来自于移动对复制的替换,这超过了 pimpl 减慢造成的开销。

对每个有意义的 Qt 类使用 pimpl idiom ,最可能让 Qt 开发者变得容易。使用 pimpl idiom
在绝大部分时间可以产生一个运行时加速——除了提供稳定的接口(二进制兼容!)还提供快速构建。在移动语义不可用的时候,Qt 是相当快的(系数:
~1.25),这超过纯 C++ 。 因此,对于所有预C++11编译器(例如: C++98, C++03),Qt
是一个不错的选择。这种优势遇到移动语义,且 pimpl idiom 进入 C++11。即使是最好的情况的 ShuffleAndSort
基准,Qt 也就只是略高于纯 C++(系数:~1.01)了。这种轻微的优势,容易被 pimpl 的开销吃掉,这样 Qt 就慢于纯 C++
(系数:~1.17)了。

这篇文章已发布。在大部分的情况下,我们将会看到结合 C++11 的移动语义和 pimpl idiom 的速度有提升。C++11 的新
unique_ptr 很容易实现 pimpl idiom 。使用 Qt 类可以替代对应的 STL (例如:QVector 和 QString
代替 std::vector 和 std::string),这样就不用给予我们任何有优势的移动语义和pimpl idiom 的组合。Qt
也有轻微的缺陷,当我们明确决定使用 pimpl idiom 的时候,每一个 Qt 类的出现,我们的代码都会引发 pimpl 的开销。

作者:leoxu, 无若, 乌合之众, htfy96, 数星星, yinahe

来源:51CTO

时间: 2024-09-25 17:30:19

最好的朋友:C++11移动语义和Pimpl手法的相关文章

c++ 11 移动语义、std::move 左值、右值、将亡值、纯右值、右值引用

为什么要用移动语义 先看看下面的代码 // rvalue_reference.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> class HugeMem { public: HugeMem(int size) : sz(size) { pIntData = new int[sz]; } HugeMem(const HugeMem & h) : sz(h.sz) { pIntData =

细数友链欺骗11招以及应有计策

想必大家都知道友链对于网站的重要性,一个好的友链可以让咱们的某个关键词排名得到迅速的提高,这点已经从最近的一篇文章中得到了证实.但是在实际操作的过程中总有那么几个人,败坏站长行业的素质,欺骗我们的友链,造成权重单向流失,我想对于任何一个站长遇到这种情况都会气炸了肺.所以今天,小苏就给大家分享一下自己知道的友链防骗术,希望让大家在交换链接的时候不再诚惶诚恐. "技术"欺骗 1 将链接代码写在html之外.正常的网页由 [Ctrl+A 全部选择 提示:你可先修改部分代码,再按运行]标签结束

慧聪网“11购”活动打造企业B2B采购盛宴

双十一"活动可谓家喻户晓,取得了巨大的成功.但"双十一"毕竟是一场面对终端消费者的采购活动,与广大的B2B生产企业并无直接联系.正是看到这一不足之处,慧聪网日前推出了"11购"活动,依托慧聪网在B2B领域的巨大优势,致力于将活动打造为一场面对企业的B2B采购盛宴. 据了解,"11购"为每月11日举行的一场低价采购盛会,采购产品含盖消费品(服装纺织.礼品家居.家居灯饰.食品茶叶.箱包饰品)与工业品(机械.五金.建材.仪器仪表)两大类.其中

陌陌的美国扩张:Blupe究竟是个啥

      Blupe的官网上写着:在现实生活中,在你的周围,在群组,发现与你志趣相投的朋友.11月22号正式上线美国App Store的一款陌生人社交应用,亲妈是陌陌. 早在2012年,陌陌就曾在美国发布过陌陌的英文版,但是似乎美国的用户并不买单,这个项目不久后被叫停了.如今,上市的陌陌更胜从前,卷土重来再次敲响美国的门.这一次,陌陌认真了:Blupe有自己的官网.社交网络账号:唐岩不仅在旧金山成立了分公司,而且由他的妻子张思川亲自操刀,担任海外事业部总裁一职. 借鉴国内的成功经验,唐岩把陌陌

大米中的“小米”

90000斤大米,10个小时,抢购一空.即便是在双十一,销量最爆的大米也不曾有这样的奇迹."一米一家",一个新品牌,不做一分钱广告,不进任何渠道,不借任何电商平台,只在微信朋友圈里销售,仅在首发日,10小时卖出90000斤,并且还在不断攀升!这样的销售奇迹,"一米一家"凭什么做到?"最好的产品加最新的模式,这是一米一家成功的诀窍!"项目的发起人郭洪驰如是说.郭洪驰,一个在电商界摸爬滚打十几年的老兵,被称为中国电商诚信第一人,今天投身到最古老的农业

9篇论文、12个workshop、2个Tutorial,谷歌是 ACL 2017上亮眼的一颗星 | ACL 2017

雷锋网AI科技评论按:计算机语言学和自然语言处理最顶尖的会议之一ACL 正在2017年7月30日至8月4日期间在加拿大温哥华举行.雷锋网 AI科技评论将赴前线带来一手报道,并对论文及大会概况进行梳理. ACL 2017中,谷歌的参与力度极大 在刚刚结束的 CVPR 2017中,来自各大科技界公司的论文就数量众多:ACL 2017 中业界公司们同样是一股重要力量.谷歌就已经发文对自己参加 ACL 2017 的各方面情况做了介绍,雷锋网(公众号:雷锋网) AI 科技评论编译如下. 作为自然语言处理和

分享个人知道的友链防骗术

摘要: 想必大家都知道友链对于网站的重要性,一个好的友链可以让咱们的某个关键词排名得到迅速的提高,这点已经从最近的一篇文章中得到了证实.但是在实际操作的过程中总有那么几个 想必大家都知道友链对于网站的重要性,一个好的友链可以让咱们的某个关键词排名得到迅速的提高,这点已经从最近的一篇文章中得到了证实.但是在实际操作的过程中总有那么几个人,败坏站长行业的素质,欺骗我们的友链,造成权重单向流失,我想对于任何一个站长遇到这种情况都会气炸了肺.所以今天,小苏就给大家分享一下自己知道的友链防骗术,希望让大家

OpenStack有容乃大

它是流行于全球云开发者中间的一种沟通语言, 它追求开放.兼容,并致力于打造普适的开源云平台, 它在数十个国家拥有成千上万的拥趸, 它就是OpenStack,声誉日隆的开源云架构. 记者的一位朋友,11月11日零时,在网上一番"血拼"之后准备支付,却因为结账的人太多而被支付宝"踢了出来",直到早上7点再次支付才取得成功.许多人感叹"双11"再破销售收入纪录时,又有谁会想到各大电商的IT基础架构承受了多大压力.为了应对业务高峰,提高基础架构的弹性,包

百度如何判断网站是否有黑帽SEO行为

百度如何判断网站是否有黑帽SEO行为 大家好,我是你们的老朋友恋星辰,前面的文章中给大家讲了一些关于自己对于SEO优化的理解,但是在6月22-28日百度的大更新中,使很多站长朋友的网站受到影响,从这次大更新中可以看出百度越来越排斥黑帽SEO的优化手法.我们在进行SEO优化时,其实有很多手法都是介于正常SEO优化和黑帽SEO之间,如果我们能够正确的掌握好SEO技术,在优化网站时控制好一个度,不去跨越百度设定的底线,这样我们的网站在优化时就会变的非常容易,同时也会得到百度蜘蛛的青睐.   为了更直接