C++对象模型及性能优化杂谈二

这篇主要介绍返回值优化及移动构造(赋值)函数,后者是C++11中的知识点。本来是不准备写的,因为这些都比较基础且编译器为我们进行了一定的优化,但还是想记录下代码层面所发生的事情,且为最近相关C++11新知识点的学习进行简单总结。

在进行下面之前,需要了解什么是构造函数,默认构造函数,拷贝构造函数,赋值运算符,析构函数,以及什么情况下编译器会为类合成在编译器看来有用的构造(和其他几个)函数,这些知识可以参考《深度探索C++对象模型》和《(More)Effective C++》。

以最基本的类开始分析:

  5 class CBase {
  6 public:
  7     CBase(const uint32_t m = 0) : m_count(m) {
  8         std::cout << "CBase ctor" << std::endl;
  9     }
 10 
 11     CBase(const CBase& lhs) {
 12         std::cout << "CBase copy ctor" << std::endl;
 13     }
 14 
 15     ~CBase() {
 16         std::cout << "CBase dtor" << std::endl;
 17     }
 18 private:
 19     uint32_t m_count;
 20 };
 21 
 22 CBase GetObjWithNoMove() {
 23     return CBase();
 24 }
 25 
 26 int main(int argc, char** argv) {
 27     CBase obj = GetObjWithNoMove();
 28     return 0;
 29 }

版本为7.3.0,直接g++ 编译,不加任何选项,输出为:

CBase ctor
CBase dtor

不考虑析构的代码,其他反汇编如下:

138 __Z16GetObjWithNoMovev:
139 100000d34:  55  pushq   %rbp
140 100000d35:  48 89 e5    movq    %rsp, %rbp
141 100000d38:  48 83 ec 10     subq    $16, %rsp
142 100000d3c:  48 89 7d f8     movq    %rdi, -8(%rbp)
143 100000d40:  48 8b 45 f8     movq    -8(%rbp), %rax
144 100000d44:  be 00 00 00 00  movl    $0, %esi
145 100000d49:  48 89 c7    movq    %rax, %rdi
146 100000d4c:  e8 9b 00 00 00  callq   155

151 _main:
152 100000d57:  55  pushq   %rbp
153 100000d58:  48 89 e5    movq    %rsp, %rbp
154 100000d5b:  53  pushq   %rbx
155 100000d5c:  48 83 ec 28     subq    $40, %rsp
156 100000d60:  89 7d dc    movl    %edi, -36(%rbp)
157 100000d63:  48 89 75 d0     movq    %rsi, -48(%rbp)
158 100000d67:  48 8d 45 ec     leaq    -20(%rbp), %rax
159 100000d6b:  48 89 c7    movq    %rax, %rdi
160 100000d6e:  e8 c1 ff ff ff  callq   -63 <__Z16GetObjWithNoMovev>

结合源码分析,汇编从152〜159似乎是在准备什么参数,然后调用GetObjWithNoMove,但是从源码并没有传实参,但这里为何会这样呢?就debug看一下这几行代码是什么玩意。
启动的时候,就直接定位在=> 0x0000000100000d67 <+16>: lea -0x14(%rbp),%rax把地址mov到rax中,查看下obj地址和rbp-0x20的值为:

(gdb) p obj
$4 = {m_count = 0}
(gdb) p &obj
$5 = (CBase *) 0x7ffeefbffa8c
(gdb) i r rbp
rbp            0x7ffeefbffaa0   0x7ffeefbffaa0
(gdb) x/w 0x7ffeefbffa8c
0x7ffeefbffa8c: 0x00000000

我们new一个对象的时候,先是分配一段足够的空间,再调用构造函数,再返回地址,当然后两步可以顺序对调,这在多线程中会有些问题,不讨论。
这里先分配一段空间,然后作为参数调用函数GetObjWithNoMove,再里面进行了构造函数的调用:

   0x0000000100000d3c <+8>: mov    %rdi,-0x8(%rbp)
=> 0x0000000100000d40 <+12>:    mov    -0x8(%rbp),%rax
   0x0000000100000d44 <+16>:    mov    $0x0,%esi
   0x0000000100000d49 <+21>:    mov    %rax,%rdi
   0x0000000100000d4c <+24>:    callq  0x100000dec


(gdb) si
CBase::CBase (this=0x100000000, m=32766) at move.cpp:7
7       CBase(const uint32_t m = 0) : m_count(m) {
(gdb) disassemble 
Dump of assembler code for function CBase::CBase(unsigned int):
=> 0x0000000100000cb4 <+0>: push   %rbp
   0x0000000100000cb5 <+1>: mov    %rsp,%rbp
   0x0000000100000cb8 <+4>: sub    $0x10,%rsp
   0x0000000100000cbc <+8>: mov    %rdi,-0x8(%rbp)
   0x0000000100000cc0 <+12>:    mov    %esi,-0xc(%rbp)
   0x0000000100000cc3 <+15>:    mov    -0x8(%rbp),%rax
   0x0000000100000cc7 <+19>:    mov    -0xc(%rbp),%edx
   0x0000000100000cca <+22>:    mov    %edx,(%rax)

是编译器为我们进行了优化?那原来的过程是怎么样的呢?重新编译并加选项-fno-elide-constructors并重新执行结果如下:

CBase ctor
CBase copy ctor
CBase dtor
CBase copy ctor
CBase dtor
CBase dtor

这里解释一下,然后再进行反汇编说明。在调用GetObjWithNoMove过程中,先进行了一次构造CBase(),此时打印第一行,然后调用拷贝构造作为返回值,打印第二行,执行完此条语句后,便进行析构,即打印第三行。GetObjWithNoMove结束返回一个临时对象,并以拷贝构造obj,即第四行,执行完后析构,最后析构obj。在这个过程中生成了两个临时对象,真正有用的是最后一次,这里没有涉及到复杂的对象构造和深拷贝。
以下是部分汇编代码,进行了旁注:

Dump of assembler code for function main(int, char**):
   0x0000000100000cb5 <+0>: push   %rbp
   0x0000000100000cb6 <+1>: mov    %rsp,%rbp
   0x0000000100000cb9 <+4>: push   %rbx
   0x0000000100000cba <+5>: sub    $0x28,%rsp
   0x0000000100000cbe <+9>: mov    %edi,-0x24(%rbp)
   0x0000000100000cc1 <+12>:    mov    %rsi,-0x30(%rbp)
   0x0000000100000cc5 <+16>:    lea    -0x14(%rbp),%rax
=> 0x0000000100000cc9 <+20>:    mov    %rax,%rdi
   0x0000000100000ccc <+23>:    callq  0x100000c50 <GetObjWithNoMove()>
   0x0000000100000cd1 <+28>:    lea    -0x14(%rbp),%rdx
   0x0000000100000cd5 <+32>:    lea    -0x18(%rbp),%rax
   0x0000000100000cd9 <+36>:    mov    %rdx,%rsi
   0x0000000100000cdc <+39>:    mov    %rax,%rdi
   0x0000000100000cdf <+42>:    callq  0x100000d8a  //拷贝构造
   0x0000000100000ce4 <+47>:    lea    -0x14(%rbp),%rax
   0x0000000100000ce8 <+51>:    mov    %rax,%rdi
   0x0000000100000ceb <+54>:    callq  0x100000d96  //析构函数
   0x0000000100000cf0 <+59>:    mov    $0x0,%ebx
   0x0000000100000cf5 <+64>:    lea    -0x18(%rbp),%rax
   0x0000000100000cf9 <+68>:    mov    %rax,%rdi
   0x0000000100000cfc <+71>:    callq  0x100000d96  //析构函数

Dump of assembler code for function GetObjWithNoMove():
=> 0x0000000100000c50 <+0>: push   %rbp
   0x0000000100000c51 <+1>: mov    %rsp,%rbp
   0x0000000100000c54 <+4>: push   %rbx
   0x0000000100000c55 <+5>: sub    $0x28,%rsp
   0x0000000100000c59 <+9>: mov    %rdi,-0x28(%rbp)
   0x0000000100000c5d <+13>:    lea    -0x14(%rbp),%rax
   0x0000000100000c61 <+17>:    mov    $0x0,%esi
   0x0000000100000c66 <+22>:    mov    %rax,%rdi
   0x0000000100000c69 <+25>:    callq  0x100000d90 //构造函数
   0x0000000100000c6e <+30>:    lea    -0x14(%rbp),%rdx
   0x0000000100000c72 <+34>:    mov    -0x28(%rbp),%rax
   0x0000000100000c76 <+38>:    mov    %rdx,%rsi
   0x0000000100000c79 <+41>:    mov    %rax,%rdi
   0x0000000100000c7c <+44>:    callq  0x100000d8a  //拷贝构造
   0x0000000100000c81 <+49>:    nop
   0x0000000100000c82 <+50>:    lea    -0x14(%rbp),%rax
   0x0000000100000c86 <+54>:    mov    %rax,%rdi
   0x0000000100000c89 <+57>:    callq  0x100000d96 //析构函数

为了减少其中的构造成本,以下是编译器为这种代码优化的实现伪代码,即返回值优化,这个函数会被编译器转化为GetObjWithNoMove

void GetObjWithNoMove(CBase& obj) {
    obj.CBase::CBase();
    //do something...
}
CBase obj;
GetObjWithNoMove(obj);

如果原来是这样的形式:

CBase GetObjWithNoMove() {
    CBase tmp;
    //do something...
    return tmp;
}

还是会被优化成上面的形式,不过网上和书上有提及那种NRVO优化,用拷贝构造的形式,可能跟写法有关系:

void GetObjWithNoMove(CBase& obj) {
    tmp.CBase::CBase();
    obj.CBase::CBase(tmp); //copy ctor
    tmp.CBase::~CBase();
    return;
}

不过一般不会像上面这么写代码,再说编译器也为我们进行了优化。
具体可参考《深度探索C++对象模型》第二章的构造函数语意学。
不过这书比较老,最新版的C++11加了许多新的特性,比如移动构造,移动赋值。这些不是很难理解,我个人在工作中用C++11的机会不多,平时都只是从网上和书上,开源项目中学习到的使用方法。

以上涉及到万能引用,右值引用,和完美转发,这些可以看参考资料。

包括最新的emplace_back之类的接口直接在内存地址上构造而不是使用push_back之类的接口,后者可能生成临时对象等性能方面的原因。

当然使用emplace之类的需要自定义类带有构造函数,不然会报如下的错误:

/usr/local/Cellar/gcc@7/7.3.0/include/c++/7.3.0/ext/new_allocator.h:136:4: error: no matching function for call to 'CString::CString(int)'
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
move.cpp:16:5: note: candidate: CString::CString(const CString&)
     CString(const CString& str) {
move.cpp:16:5: note:   no known conversion for argument 1 from 'int' to 'const CString&'

还有些情况下不能使用emplace之类的,具体原因可能要在实践中去发现,比如参考中的会涉及,但是看的云里雾里的。
emplace_back部分源码如下:

template<class _Objty,
        class... _Types>
        static void construct(_Alloc&, _Objty * const _Ptr, _Types&&... _Args)
        {   // construct _Objty(_Types...) at _Ptr
        ::new (const_cast<void *>(static_cast<const volatile void *>(_Ptr)))
            _Objty(_STD forward<_Types>(_Args)...);
        }

还是要多学习,和运用。
这篇没啥干货,后面会备忘C++11中的一些实现源码。

参考资料:
《深度探索C++对象模型》
《Effective Modern C++》
C++雾中风景9:emplace_back与可变长模板

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容