同样是 return 一个中间变量,为什么另一种代码的反汇编分配了两次空间? - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
amiwrong123
V2EX    程序员

同样是 return 一个中间变量,为什么另一种代码的反汇编分配了两次空间?

  •  
  •   amiwrong123 2019-07-14 17:07:20 +08:00 3525 次点击
    这是一个创建于 2337 天前的主题,其中的信息可能已经有所发展或是发生改变。

    总所周知,函数体返回值那里没有&,就会返回一个中间变量。

    class sale { public: int i = 1; }; sale add(const sale& lift, const sale& right) { sale sum = lift; sum.i += right.i; return sum; } int main() { sale one; sale two; const sale& global = add(one, two); } 

    假设有如上代码,进行反汇编后,汇编如下:

     const sale& global = add(one, two); 00D51A32 8D 45 E8 lea eax,[two] 00D51A35 50 push eax 00D51A36 8D 4D F4 lea ecx,[one] 00D51A39 51 push ecx 00D51A3A E8 40 F9 FF FF call add (0D5137Fh) 00D51A3F 83 C4 08 add esp,8 #调用完毕后,清除栈空间 00D51A42 89 85 04 FF FF FF mov dword ptr [ebp-0FCh],eax #为中间变量分配空间 00D51A48 8B 95 04 FF FF FF mov edx,dword ptr [ebp-0FCh] 00D51A4E 89 55 D0 mov dword ptr [ebp-30h],edx #把中间变量复制给新的变量 00D51A51 8D 45 D0 lea eax,[ebp-30h] #导入新变量的地址 00D51A54 89 45 DC mov dword ptr [global],eax #把这个地址给 global,因为引用本质是指针 

    上面写的注释不一定对,但为什么这里要分配两次空间呢? 如果我换一个简单的程序:

    int re() { return 5; } int main() { //int a = 1; const int& b = re(); } 

    他的汇编就和我想象中一样了,只分配一次空间:

     const int& b = re(); 00B019B2 E8 0E FA FF FF call re (0B013C5h) 00B019B7 89 45 E8 mov dword ptr [ebp-18h],eax #分配空间 00B019BA 8D 45 E8 lea eax,[ebp-18h] #把地址导入 eax 00B019BD 89 45 F4 mov dword ptr [b],eax #把 eax 赋值给 b,因为 b 是引用,相当于指针 
    36 条回复    2019-07-15 20:25:05 +08:00
    lhx2008
        1
    lhx2008  
       2019-07-14 17:15:02 +08:00
    我猜,关掉编译器优化,反汇编之后结果就一样了
    ipwx
        2
    ipwx  
       2019-07-14 17:23:04 +08:00
    首先,楼主你两段代码都是不合法的。

    对于不合法的代码,C++ 编译器没有义务给你吐出合理的结果。
    stephen9357
        3
    stephen9357  
       2019-07-14 17:23:24 +08:00
    debug 编译的代码,编译器怎么顺手怎么来,别当真。甚至用 IDA 看系统自带的 release 动态库时,也遇到过类似情况,编译器毕竟不能保证每一行代码编译后都是最优的,能看明白就可以了。
    amiwrong123
        4
    amiwrong123  
    OP
       2019-07-14 17:36:22 +08:00
    @lhx2008
    用得是 vs2017,找了找,在当前项目的什么设置里面,找到了“优化”,里面有什么内联函数拓展、启动内部函数什么的,但基本都是关着的。

    @ipwx
    const int& b = re();原来这种用法是不合法的吗?有点没懂啊,我知道如果返回局部变量的引用,这种情况是不合法的,虽然编译器只是报个 warning。


    @stephen9357
    原来是这样的啊。确实大概能看明白,比如函数体返回值那里有没有&(返回的是不是引用),会体现到汇编上去。虽然分配了两次空间,但可能就是编译器没优化好呗。
    thedrwu
        5
    thedrwu  
       2019-07-14 17:36:33 +08:00
    首先,不能这样写。你让变量活在哪里?

    至于楼主的问题, 对向返回的是一个地址. 最后到 edx 里的是地址的地址,即 %ebp-30h 这个数字。中间变量没毛病。
    至于第二段, 不是地址的地址, 而只有一层地址。

    没仔细看,仅供参考
    akira
        6
    akira  
       2019-07-14 17:47:02 +08:00
    第一个是类吧 类应该是需要 2 个指针来表达的
    hoyixi
        7
    hoyixi  
       2019-07-14 17:49:56 +08:00
    'lift' and right
    amiwrong123
        8
    amiwrong123  
    OP
       2019-07-14 17:57:34 +08:00
    @thedrwu
    好吧,首先是不是,引用绑定到返回的中间变量,这种写法就是错的吗

    然后,我又改了一下,改成 sale global = add(one, two);汇编就变成了:
    00B019D2 8D 45 E8 lea eax,[two]
    00B019D5 50 push eax
    00B019D6 8D 4D F4 lea ecx,[one]
    00B019D9 51 push ecx
    00B019DA E8 F0 F9 FF FF call add (0B013CFh)
    00B019DF 83 C4 08 add esp,8
    00B019E2 89 85 10 FF FF FF mov dword ptr [ebp-0F0h],eax
    00B019E8 8B 95 10 FF FF FF mov edx,dword ptr [ebp-0F0h]
    00B019EE 89 55 DC mov dword ptr [global],edx
    好像跟是不是对象没关系,只用一个地址就好了。
    哎,我是不是有点钻牛角尖了,但是又有点好奇。

    @akira
    你看上面的汇编,好像跟是不是对象没关系啊。

    @hoyixi
    哈哈哈,一时手滑啦
    aliwalker
        9
    aliwalker  
       2019-07-14 18:15:24 +08:00
    我用 clang 编译了一下第一段,发现没有写把 add 返回值写两次内存的操作...

    100000f5a: 48 8d 7d f8 leaq -8(%rbp), %rdi # &one
    100000f5e: 48 8d 75 f0 leaq -16(%rbp), %rsi # &two
    100000f62: e8 a9 ff ff ff callq -87 <__Z3addRK4saleS1_> # call add
    100000f67: 31 c9 xorl %ecx, %ecx # 清零
    100000f69: 89 45 e0 movl %eax, -32(%rbp) # 返回值存到临时变量
    100000f6c: 48 8d 75 e0 leaq -32(%rbp), %rsi # 指针
    100000f70: 48 89 75 e8 movq %rsi, -24(%rbp) # 指针值存到 global
    100000f74: 89 c8 movl %ecx, %eax # 返回值为 0
    100000f76: 48 83 c4 20 addq $32, %rsp
    100000f7a: 5d popq %rbp
    100000f7b: c3 retq

    用 const 引用返回值是可以的,这个临时变量在 call site 的 frame 上是有分配空间的。如果改成 sale &global = add(one, two);就不行了:initial value of reference to non-const must be an lvalue。

    第二段结果是一样的,只是生成的是 x64 机器码。
    aliwalker
        10
    aliwalker  
       2019-07-14 18:21:56 +08:00
    补充一下,从第二段反汇编出来的内容可以看到为什么不是 const 引用不行:

    _main:
    100000f90: 55 pushq %rbp
    100000f91: 48 89 e5 movq %rsp, %rbp
    100000f94: 48 83 ec 10 subq $16, %rsp
    100000f98: e8 e3 ff ff ff callq -29 <__Z2rev>
    100000f9d: 31 c9 xorl %ecx, %ecx
    100000f9f: 89 45 f4 movl %eax, -12(%rbp)
    100000fa2: 48 8d 55 f4 leaq -12(%rbp), %rdx
    100000fa6: 48 89 55 f8 movq %rdx, -8(%rbp)
    100000faa: 89 c8 movl %ecx, %eax
    100000fac: 48 83 c4 10 addq $16, %rsp
    100000fb0: 5d popq %rbp
    100000fb1: c3 retq

    返回的 int 是 4bytes,写在-12(%rbp)上,但是指针 b 的位置-8(%rbp)其实和这个返回的 temp 值重合。
    zjsxwc
        11
    zjsxwc  
       2019-07-14 18:33:46 +08:00 via Android
    难道没人和我一样奇怪
    sum 在栈里的内容在函数结束时会不会被释放吗
    ipwx
        12
    ipwx  
       2019-07-14 18:43:05 +08:00
    @amiwrong123 不合法的理由,见 5L 和 11L 的疑惑。
    ipwx
        13
    ipwx  
       2019-07-14 18:45:31 +08:00
    另外我大概理解楼主为什么要写不合法代码的理由了,是想研究 C++ 返回值地址的问题嘛?

    但是 C++ 编译器会根据返回值的赋值进行代码优化的。

    比如:

    class A { ... };

    A someFunction() { A a; return a; }

    A target = someFunction();

    在深度优化的时候不会发生拷贝,直接在返回值 target 上调用构造函数。
    zjsxwc
        14
    zjsxwc  
       2019-07-14 18:46:42 +08:00 via Android
    In C++, unlike in C#, struct makes few differences with class. A struct is a class whose default visibility is public. Whether the allocation is performed on the stack or in the heap depends on the way you allocate your instance

    class A;

    void f()
    {
    A a;//stack allocated
    A *a1 = new A();// heap
    }
    zjsxwc
        15
    zjsxwc  
       2019-07-14 18:48:55 +08:00 via Android
    @zjsxwc 楼主的 sum 内存会被释放
    aliwalker
        16
    aliwalker  
       2019-07-14 18:50:15 +08:00 via Android
    @ipwx yep. Return value optimization. 是 copy elision 的一种
    lcdtyph
        17
    lcdtyph  
       2019-07-14 20:27:46 +08:00   1
    @ipwx #12
    @thedrwu #5

    两个都是合法的,临时变量在绑定到常引用之后生命周期会被延长,参见 https://en.cppreference.com/w/cpp/language/lifetime

    在 c++11 右值出现之前都是这样做的。
    amiwrong123
        18
    amiwrong123  
    OP
       2019-07-14 20:50:15 +08:00
    @aliwalker
    既然你用 clang 编译没有出现两次写内存,那可能我的编译器的问题吧。
    然后你的第二段,我看了,它把返回值 int 存在了-12,-11,-10,-9 这四个字节里,然后把地址存在了-8,-7,...,-1,没有什么重合啊感觉。所以没理解,“为什么不是 const 引用不行”。
    amiwrong123
        19
    amiwrong123  
    OP
       2019-07-14 20:57:03 +08:00
    @ipwx
    是啊,你说的差不多。主要把,我是想看看 return 到底是怎么 return 的,有几次内存拷贝,这样。
    还有最后你说的这个深度优化,意思懂啦,相当于直接 A target;
    amiwrong123
        20
    amiwrong123  
    OP
       2019-07-14 20:59:17 +08:00
    @zjsxwc
    你们都说,内存会被释放掉。那么我打印 global 的值的时候,肯定就是非法值呗。
    class sale {
    public:
    int i = 1;
    };

    sale add(const sale& lift, const sale& right) {
    sale sum = lift;
    sum.i += right.i;
    return sum;
    }

    int main()
    {
    sale one;
    sale two;
    const sale& global = add(one, two);
    cout << global.i;
    }
    执行这个代码,我发现还是能打印出来 2 啊,也不是什么非法值。
    nethard
        21
    nethard  
       2019-07-14 21:20:09 +08:00 via iPhone
    哈哈,楼主要是这么喜欢这样写,返回一个栈上的指针,可以去写 go 玩玩。
    lcdtyph
        22
    lcdtyph  
       2019-07-14 21:25:17 +08:00
    @amiwrong123 #18
    clang 有个参数 -fno-elide-constructors 可以让编译器不做 rvo,这样在你第一种代码里一定会分配两次内存。默认是会做 rvo 的。
    visual studio 的编译器不知道有没有类似的选项……
    ispinfx
        23
    ispinfx  
       2019-07-14 21:27:50 +08:00 via iPhone
    好奇那么多说不合法的…这不是常引用最常见的用法吗
    lcdtyph
        24
    lcdtyph  
       2019-07-14 21:32:57 +08:00
    还有实际上 lz 的情况在 c++17 中一定不会产生临时对象,这是 c++17 新的 guaranteed copy elision 特性来保证的。
    amiwrong123
        25
    amiwrong123  
    OP
       2019-07-14 21:50:42 +08:00
    @lcdtyph
    看到啦,在 Temporary object lifetime 的 There are two exceptions from that:,我这个属于 const lvalue reference。

    你说的-fno-elide-constructors,这又是我的知识盲区了。。。看了看博客,意思就是会省略一些中间变量的创建,因为两次拷贝和一次拷贝效果是一样的。但为啥你说,我第一种代码里一定会分配两次内存,不是默认是会优化的吗? vs2017 里面好像没有这个选项。。

    guaranteed copy elision,又是一个知识点,我拿小本本记上。
    amiwrong123
        26
    amiwrong123  
    OP
       2019-07-14 22:00:39 +08:00
    @nethard
    也不是喜欢这么写,只是在测试想把各种情况测试一遍,带不带 const,带不带&,这样


    @ispinfx
    开始这个用法对不对还真拿不准,然后就很疑惑。不过看了 17 楼的文档里写了这种用法。
    lcdtyph
        27
    lcdtyph  
       2019-07-14 22:02:31 +08:00 via iPhone
    @amiwrong123
    这个选项是强制在可以省略临时对象的情况下调用拷贝构造,所以打开这个选项就相当于没了返回值优化,就会造成一次额外的拷贝
    ispinfx
        28
    ispinfx  
       2019-07-14 22:36:11 +08:00 via iPhone
    @amiwrong123 这个我记得以前上谭 cpp primer 里就有说的吧,虽然我已不写 cpp10 年了。。
    karia
        29
    karia  
       2019-07-14 22:39:29 +08:00 via Android
    《程序员的自我修养》内存那一章(11 章?忘了)讲到过这个问题

    return 栈上大型结构的时候,caller 的 stack frame 里会分配 2 个临时空间,一个隐式的用来保存返回值,返回之后再 memcpy 给你的显式声明的 global

    另外提醒楼主...乐于尝试是不错,但是不要钻牛角尖了...孔夫子曰过思而不学则殆...多看书,或许很多问题别人已经研究过了
    lrxiao
        30
    lrxiao  
       2019-07-15 07:39:35 +08:00
    在 debug 模式看汇编 yy 分配空间... 也可能只是中间变量没优化而已..
    你需要的是写个 destructor 然后看调用次数..
    lrxiao
        31
    lrxiao  
       2019-07-15 07:45:49 +08:00
    这两个都是 copy elision/rvo 后的结果了 并没有额外的 constructor/destructor 介入
    14m3
        32
    14m3  
       2019-07-15 09:25:58 +08:00
    1. 楼主,以后可以在 https://godbolt.org/(一个网页版的交互式编译器)上面来测试,可以选择不同的编译器,开启不同的编译选项,也能很方便看到汇编代码
    2. 前面 @lcdtyph 已经说过了,const lvalue reference 会延长临时对象的生命周期,所以问题中的代码是合法的
    3. 其实讨论这个代码,是需要确定讨论的环境的,是 C++11/14,还是 C++17。因为 C++17 标准中添加了 Guaranteed Copy Elision,重新定义了 value category 中 prvalues 的语义
    4. 我自己测试了一下 https://godbolt.org/z/NRKI-R,在 C++17 标准下,不管 main 函数中是 const sale& global = add(one, two); 还是 const sale global = add(one, two); 问题中的代码都是调用了三次构造函数,前两次构造函数是 sale one; 和 sale two;,第三次构造函数是拷贝构造。在其他标准下,楼主可以自己测试.
    amiwrong123
        33
    amiwrong123  
    OP
       2019-07-15 13:50:13 +08:00
    @lcdtyph
    好吧,懂啦,看来这个编译器选项也挺重要啊。

    @ispinfx
    我就正在 primer 呢,但看得断断续续,我是因为看到书中某页的一句话“返回的是 sum 的副本”,但没细说,然后就开始想这个问题。
    amiwrong123
        34
    amiwrong123  
    OP
       2019-07-15 14:08:34 +08:00
    @karia
    这本书买了,还没看。的确很多问题别人已经研究过了,所以一般有问题我都是先看网上博客啥的

    @lrxiao
    你说,写个 destructor 然后看调用次数,这个挺好使啊。我在 32 楼给的链接里看到了调用析构的次数了。
    你意思这两段代码都是已经被优化了的呗
    amiwrong123
        35
    amiwrong123  
    OP
       2019-07-15 14:12:02 +08:00
    @14m3
    哇,你这个链接是个神器,回头好好研究下。确实,这个代码怎么优化的跟环境有关系啊。你这个代码而且写得很清楚,加了构造和析构的打印后,思路瞬间清晰了。
    14m3
        36
    14m3  
       2019-07-15 20:25:05 +08:00
    @amiwrong123 嗯嗯,互相学习 :)
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     891 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 19:59 PVG 03:59 LAX 11:59 JFK 14:59
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86