C++的返回值优化(RVO,Return Value Optimization)

从函数返回值

为了从一个函数得到运行结果,常规的途径有两个:通过返回值和通过传入函数的引用或指针(当然还可以通过全局变量或成员变量,但这显然算不上是什么好主意)。

通过传给函数一个引用或指针来承载返回值在很多情况下是无可厚非的,毕竟有时函数需要将多个值返回给用户(C++17后可以使用tuple + 结构化绑定)。除了这种情况之外,应当尽量做到参数作为函数输入,返回值作为函数输出(这不是很自然的事情吗?)。不过我们总能看到一些“突破常规”的做法:

首先定义Message类:

struct Message
{
    int a;
    int b;
    int c;
    int d;
    int e;
    int f;
};

为了从某个地方(比如一个队列)得到一个特定Message对象,有些人喜欢写一个这样的getMessage:

void getMessage(Message &msg); // 形式1

虽然只有一个返回值,但仍然是通过传入函数的引用返回给调用者的。

为什么要这样呢?“嗯,为了提高性能。你知道,要是这样定义函数,返回Message对象时必须要构造一个临时对象,这对性能有影响。”

Message getMessage(); // 形式2

我们先不讨论这带来了多少性能提升,先看看形式1相对形式2带来了哪些弊端。我认为有两点:

1. 可读性变差

略(希望你能和我一样认为这是显而易见的)。

2. 将对象的初始化划分成了两个步骤

调用形式1时,你必然要这样:

Message msg;     // S1
getMessage(msg); // S2

这给维护者带来了犯错的机会:一些需要在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是有限制条件的,在某些情况下无法进行优化,在一篇关于MSVC2005的RVO技术的文章中,提到了3点导致无法优化的情况:

1. 函数抛异常

关于这点,我是有疑问的。文章中说如果函数抛异常,开不开RVO结果都一样。如果函数抛异常,无法正常的返回,我当然不会要求编译器去做RVO了。

2. 函数可能返回具有不同变量名的对象

例如:

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

3. 函数有多个出口

例如:

Message getMessage_NoRVO2(int in)
{
    Message msg;
    if (in % 2)
    {
        return msg;
    }
    msg.a = 1;
    return msg;
}

这个在GCC上验证发现RVO仍然生效,查看汇编发现只有一个retq指令,多个出口被优化成一个了。


参考文档:

https://en.wikipedia.org/wiki/Copy_elision#cite_ref-moreeffcpp_6-1

https://en.cppreference.com/w/cpp/language/copy_elision

https://docs.microsoft.com/en-us/previous-versions/ms364057(v=vs.80)

  • 10
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
### 回答1: rvo2是一款流行的多体仿真引擎,其queryvisibility函数主要用于查询两个Agent之间是否存在可视障碍。可视障碍指的是视线被遮挡而无法直接看到对方的情况。这个函数的输入参数包括两个Agent的代号,以及它们所在位置的坐标信息等。当函数被调用时,rvo2会基于这些信息来判断两个Agent之间是否存在可视障碍。 对于具体的实现方法,RVO2是通过将两个Agent看作固定半径的圆来实现的。在判断两个Agent之间是否存在可视障碍时,RVO2会检测以两个圆为圆心、圆心距离为直径的大圆,然后依次检测该大圆上的若干个点是否被障碍物遮挡。如果存在可视障碍,则两个Agent之间的可视距离被限制为大圆上两个点之间的线段长度。 最终,queryvisibility函数将返回一个bool类型的值,用于表示两个Agent之间是否存在可视障碍。这个函数在多个应用场景中都有广泛的应用,如人群模拟、机器人导航等。在实际应用中,我们往往可以通过修改一些参数来改变这个函数的表现,以满足不同的需求。 ### 回答2: rvo2是一个用于人群模拟和路径规划的库。queryvisibility是rvo2库中的一个函数,用于查询在可行路径中是否存在障碍物。 在rvo2库中,人群的移动路径是通过导航网格(navigation mesh)来表示的。在该网格中,障碍物被定义为无法穿过的区域,并用多边形表示。 使用queryvisibility函数时,会根据给定的起点和终点,检查两者之间是否存在障碍物。如果存在障碍物,则返回false,表示两点之间不可行;如果不存在障碍物,则返回true,表示两点之间可行。 这个函数的作用在人群模拟和路径规划中非常重要。当人群移动时,需要根据当前位置和目标位置,查询路径上是否有障碍物。如果路径上存在障碍物,则需要通过其他路径避免碰撞和拥挤。 总的来说,queryvisibility是rvo2库中一个用于路径检测的重要函数,确保人群可以在避开障碍物的情况下顺利移动。 ### 回答3: rvo2 queryvisibility是一个查询函数,用于在RVO2库中计算两个移动物体之间的可见性。该函数主要用于行人和机器人等移动物体的路径规划和避障等问题。 该函数使用了远程可视化障碍(remote visibility obstacles,简称RVO)的概念,计算两个移动物体之间的RVO集合,即可视区域。如果两个移动物体之间存在可行的路径,则它们之间的RVO集合为空,否则为非空。 RVO2 queryvisibility函数的返回值为布尔类型,表示两个移动物体之间是否存在可行路径。该函数需要传入两个移动物体的位置和半径等参数,还可以传入一些可选参数,如最大查询距离和最大查询时间等。 总之,rvo2 queryvisibility是一个在RVO2库中非常重要的查询函数,它能够帮助程序员计算出两个移动物体之间的可见区域,从而实现行人和机器人等移动物体的路径规划和避障等功能。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值