一、函数返回值
为了从一个函数得到运行结果,常规的途径有两个:通过返回值和通过传入函数的引用或指针(当然还可以通过全局变量或成员变量,当然这算不上最优的方案)。通过传给函数一个引用或指针来承载返回值在很多情况下是无可厚非的,毕竟有时函数需要将多个值返回给用户。 除了这种情况之外,我觉得应当尽量做到参数作为函数输入,返回值作为函数输出。然而,总能看到一些“突破常规”的做法。首先定义 Message 类:
struct Message {
int a;
int b;
int c;
int d;
int e;
int f;
} ;
为了从某个地方(比如一个队列)得到一个特定 Message 对象,有些人喜欢写一个这样的 getMessage:
void getMessage ( Message & msg) ;
虽然只有一个返回值,但仍然是通过传入函数的引用返回给调用者的。为什么要这样呢?为了提高性能,要是这样定义函数,返回 Message 对象时必须要构造一个临时对象,这对性能有影响。
Message getMessage ( ) ;
先不讨论这带来了多少性能提升,先看看形式 1 相对形式 2 带来了哪些弊端,这里有两点:第一是可读性变差,第二就是将对象的初始化划分成了两个步骤,调用形式 1 时,必然要如下的这样:
Message msg;
getMessage ( msg) ;
这给维护者带来了犯错的机会:一些需要在 S2 语句后面对 msg 进行的操作有可能会被错误的放在 S1 和 S2 之间。 如果是形式 2,维护者就不可能犯这种错误:
Message msg = getMessage ( ) ;
现在来看性能,形式 2 真的相对形式 1 性能更差吗?对于下面的代码:
# include <stdio.h>
struct Message {
Message ( ) {
printf ( "Message::Message() is called\n" ) ;
}
Message ( const Message & ) {
printf ( "Message::Message(const Message &msg) is called\n" ) ;
}
Message& operator = ( const Message & ) {
printf ( "Message::operator=(const Message &) is called\n" ) ;
}
~ Message ( ) {
printf ( "Message::~Message() is called\n" ) ;
}
int a;
int b;
int c;
int d;
int e;
int f;
} ;
Message getMessage ( ) {
Message result;
result. a = 0x11111111 ;
return result;
}
int main ( ) {
Message msg = getMessage ( ) ;
return 0 ;
}
Message :: Message ( ) is called
Message :: Message ( const Message & msg) is called
Message :: ~ Message ( ) is called
Message :: ~ Message ( ) is called
其中,第一行是临时对象 result 构造时打印,第二行是将临时对象赋给 msg 时打印,第三行是临时对象 result 析构时打印,第四行是 msg 析构时打印。然而使用 GCC 7.3.0 版本使用 O0(即关闭优化)编译上述代码后,运行结果为:
Message :: Message ( ) is called
Message :: ~ Message ( ) is called
并没有像预期的输出那样,如果使用 MSVC2017 编译,且关闭优化(/Od),确实可以得到预期输入,但是一旦打开优化(/O2),输出就和 GCC 的一样。来看看实际上生成了什么代码(使用 GCC 编译):
( gdb) disassemble main
Dump of assembler code for function main ( ) :
0x0000000000000776 < + 0 > : push % rbp
0x0000000000000777 < + 1 > : mov % rsp, % rbp
0x000000000000077a < + 4 > : push % rbx
0x000000000000077b < + 5 > : sub $0x28 , % rsp
0x000000000000077f < + 9 > : mov % fs: 0x28 , % rax
0x0000000000000788 < + 18 > : mov % rax, - 0x18 ( % rbp)
0x000000000000078c < + 22 > : xor % eax, % eax
0x000000000000078e < + 24 > : lea - 0x30 ( % rbp) , % rax #将栈上地址- 0x30 ( % rbp) 传给getMessage函数
0x0000000000000792 < + 28 > : mov % rax, % rdi
0x0000000000000795 < + 31 > : callq 0x72a < getMessage ( ) >
0x000000000000079a < + 36 > : mov $0x0 , % ebx
0x000000000000079f < + 41 > : lea - 0x30 ( % rbp) , % rax
0x00000000000007a3 < + 45 > : mov % rax, % rdi
0x00000000000007a6 < + 48 > : callq 0x7e4 < Message :: ~ Message ( ) >
0x00000000000007ab < + 53 > : mov % ebx, % eax
0x00000000000007ad < + 55 > : mov - 0x18 ( % rbp) , % rdx
0x00000000000007b1 < + 59 > : xor % fs: 0x28 , % rdx
0x00000000000007ba < + 68 > : je 0x7c1 < main ( ) + 75 >
0x00000000000007bc < + 70 > : callq 0x5f0 < __stack_chk_fail@plt>
0x00000000000007c1 < + 75 > : add $0x28 , % rsp
0x00000000000007c5 < + 79 > : pop % rbx
0x00000000000007c6 < + 80 > : pop % rbp
0x00000000000007c7 < + 81 > : retq
End of assembler dump.
( gdb) disassemble getMessage
Dump of assembler code for function getMessage ( ) :
0x000000000000072a < + 0 > : push % rbp
0x000000000000072b < + 1 > : mov % rsp, % rbp
0x000000000000072e < + 4 > : sub $0x20 , % rsp
0x0000000000000732 < + 8 > : mov % rdi, - 0x18 ( % rbp) #将main函数传入的栈上地址保存到- 0x18 ( % rbp) 处
0x0000000000000736 < + 12 > : mov % fs: 0x28 , % rax
0x000000000000073f < + 21 > : mov % rax, - 0x8 ( % rbp)
0x0000000000000743 < + 25 > : xor % eax, % eax
0x0000000000000745 < + 27 > : mov - 0x18 ( % rbp) , % rax #将main函数传入的栈上地址传给Message :: Message ( ) 函数
0x0000000000000749 < + 31 > : mov % rax, % rdi
0x000000000000074c < + 34 > : callq 0x7c8 < Message :: Message ( ) >
0x0000000000000751 < + 39 > : mov - 0x18 ( % rbp) , % rax
0x0000000000000755 < + 43 > : movl $0x11111111 , ( % rax)
0x000000000000075b < + 49 > : nop
0x000000000000075c < + 50 > : mov - 0x18 ( % rbp) , % rax
0x0000000000000760 < + 54 > : mov - 0x8 ( % rbp) , % rdx
0x0000000000000764 < + 58 > : xor % fs: 0x28 , % rdx
0x000000000000076d < + 67 > : je 0x774 < getMessage ( ) + 74 >
0x000000000000076f < + 69 > : callq 0x5f0 < __stack_chk_fail@plt>
0x0000000000000774 < + 74 > : leaveq
0x0000000000000775 < + 75 > : retq
End of assembler dump.
可以看出来,在 getMessage 函数中构造的对象实际上位于 main 函数的栈帧上,并没有额外构造一个 Message 对象。这是因为开启所谓的返回值优化(RVO,Return Value Optimization)的缘故,能想得到的效果编译器已经自动完成。
二、RVO
对于用户来说,RVO 并不是什么特别复杂的机制,主流的 GCC 和 MSVC 均支持,也没什么特别需要注意的地方。它存在的目的是优化掉不必要的拷贝复制函数的调用,即使拷贝复制函数有什么副作用,例如上面代码中的打印语句,这可能是唯一需要注意的地方。 从上面的汇编代码中可以看出来,在 GCC 中,其基本手段是直接将返回的对象构造在调用者栈帧上,这样调用者就可以直接访问这个对象而不必复制。RVO 是有限制条件的,在某些情况下无法进行优化,这里有 3 点导致无法优化的情况。
① 函数抛异常
如果函数抛异常,开不开 RVO 结果都一样,如果函数抛异常,无法正常的返回,我们当然不会要求编译器去做 RVO 。
② 函数可能返回具有不同变量名的对象
Message getMessage_NoRVO1 ( int in) {
Message msg1;
msg1. a = 1 ;
Message msg2;
msg2. a = 2 ;
if ( in % 2 ) {
return msg1;
} else {
return msg2;
}
}
经过验证,在 GCC 上确实也是这样的,拷贝构造函数被调用,但这种情况在很多时候应该都是可以通过重构避免的。
Message :: Message ( ) is called
Message :: Message ( ) is called
Message :: Message ( const Message & msg) is called
Message :: ~ Message ( ) is called
Message :: ~ Message ( ) is called
Message :: ~ Message ( ) is called
③ 函数有多个出口
Message getMessage_NoRVO2 ( int in) {
Message msg;
if ( in % 2 ) {
return msg;
}
msg. a = 1 ;
return msg;
}
这个在 GCC 上验证发现 RVO 仍然生效,查看汇编发现只有一个 retq 指令,多个出口被优化成一个。