IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 编译器的RVO优化和std::move -> 正文阅读

[C++知识库]编译器的RVO优化和std::move

1,前言

在修改基础库中的接口时,发现有很多地方都用到了std::move来转换返回值,比如下面这个代码:

std::string GetOsName() {
    std::string ret;
    return std::move(ret);
}

可能当初编写者的原意是减少一次std::string的拷贝构造,但是却忽略了编译器自身所带的返回值优化策略,导致画蛇添足。

在 C++11 之前,返回一个本地对象意味着这个对象会被拷贝,除非编译器发现可以做返回值优化(named return value optimization,或 NRVO),能把对象直接构造到调用者的栈上。

从 C++11 开始,返回值优化仍可以发生,但在没有返回值优化的情况下,编译器将试图把本地对象移动出去,而不是拷贝出去。这一行为不需要程序员手工用 std::move 进行干预——使用 std::move 对于移动行为没有帮助,反而会影响返回值优化。

而不同的编译模式(Debug或Release)下,编译器所采用的优化策略也不一样,下面深入介绍一下编译器的RVO (return value optimization) 和NRVO (named return value optimization) 。

2,RVO和NRVO

RVO即Return Value Optimization,是一种编译器优化技术,可以把通过函数返回创建的临时对象给”去掉”,然后可以达到少调用拷贝构造的操作。

NRVO和RVO功能类似,也是用作返回值优化,但是它们也有一些区别,看下面这段代码:

class Obj {
 
};
 
//NRVO
Obj test_nrvo()
{
    Obj obj;
    return obj;
}
 
//RVO
Obj test_rvo()
{
    return  Obj();
}

简单来说,NRVO只是RVO的一种变体,我们使用时并不需要在的具体区分上花太多时间,只需知道它们的作用都是用于返回值优化就可以了。

接下来我们在gcc4.9.4的小端机器上进行演示。

我们先将上面的代码补充完整main.cc:

class Obj {
public:
    Obj()
    {
        cout << "Obj()" << endl;
    }
    Obj(const Obj&)
    {
        cout << "Obj(const Obj&)"
            << endl;
    }
    ~Obj()
    {
        cout << "~Obj()" << endl;
    }
};
 
//NRVO
Obj test_nrvo()
{
    Obj obj;
    return obj;
}
 
//RVO
Obj test_rvo()
{
    return  Obj();
}
 
int main()
{
    cout << "*** 1 ***" << endl;
    auto obj1 = test_nrvo();
    cout << "*** 2 ***" << endl;
    auto obj2 = test_rvo();
   cout << "*** 3 ***" << endl;
     
    return 0;
}

编译命令:g++ main.cc -std=c++11

输出结果为:

//带RVO的输出
*** 1 ***
Obj()
*** 2 ***
Obj()
*** 3 ***
~Obj()
~Obj()

我们主要看《*** 3 ***》之上的输出,RVO和NRVO都只是调用了一次普通的构造函数,并没有想象中会调用拷贝构造函数,这是因此gcc编译器帮我们默认开启了RVO优化。

去掉RVO优化,再次编译:g++ main.cc -std=c++11 -fno-elide-constructors

输出结果为:

//不带RVO的输出
*** 1 ***
Obj()              //obj构造,默认构造函数
Obj(const Obj&)    //test_nrvo函数中用obj对象拷贝构造临时对象_temp
~Obj()             //test_nrvo函数返回时,obj临时对象析构
Obj(const Obj&)    //main函数中用test_nrvo函数中拷贝构造的临时对象_temp拷贝构造对象 obj1
~Obj()             //析构临时对象_temp
*** 2 ***
Obj()
Obj(const Obj&)
~Obj()
Obj(const Obj&)
~Obj()
*** 3 ***
~Obj()
~Obj()

可见当类的成员变量较复杂,拷贝开销较大时,编译器的RVO优化可以帮助我们提高程序的运行速度。

但是当我们稍微修改一下代码,结果又会完全不同:

Obj test_case(int n)
{
   Obj obj_1, obj_2;
    if (n > 0)
        return obj_1;
    else
        return obj_2;
}
 
int main()
{
    cout << "*** 0 ***" << endl;
    auto obj0 = test_case(1);
    cout << "*** 1 ***" << endl;
    auto obj1 = test_nrvo();
    cout << "*** 2 ***" << endl;
    auto obj2 = test_rvo();
    cout << "*** 3 ***" << endl;
     
    return 0;
}

输出结果为:

*** 0 ***
Obj()
Obj()
Obj(Obj &&)
~Obj()
~Obj()
*** 1 ***
Obj()
*** 2 ***
Obj()
*** 3 ***

我们发现竟然调用了拷贝构造函数,这跟上面说的RVO完全不一样,代码差别也仅仅只是多了一个if else。

这就要说一下RVO的实现原理了,在没有RVO优化的情况下,一个函数是会在自己的堆栈帧中为返回值分配空间的;

而在RVO优化开启的时候,函数共用了父堆栈帧来避免复制,当我们添加if else 分支时,编译器不知道要放置哪个返回值。

3,std::move VS RVO

std::move是c11引入的新语法,很多人误解了std::move的用法,导致在错误的情况下使用了它,我们先来看一下std::move的实现(gcc4.9.4):

template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

std::move的关键作用是将入参转换为右值,它还指示编译器可以移动对象,而不移动任何东西。

移动的代价远远小于拷贝,但是比RVO高,它主要做两件事:

1,“窃取”所有的数据

2,“欺骗“我们所”窃取“的对象忘掉一切。

如果我们想指示编译器移动,可以定义移动构造函数和移动赋值运算符。比如在上面的代码中加上移动构造函数:

Obj(Obj &&)
    {
        cout << "Obj(Obj &&)"
            << endl;
    }

那么std::move是否是一直有益的呢?

如果编译器可以做 RVO,那么 RVO。否则,编译器调用移动构造函数;

但是理想总是和现实背道而驰,我们看一下下面这个函数:

Obj test_move()
{
    Obj obj;
    return std::move(obj);
}

大家不妨先推演一下结果,是否和test_nrvo一样,仅仅只是调用了一次默认构造函数?

输出结果为:

Obj()
Obj(Obj &&)
~Obj()

出乎意料或者说意料之中,多调用了一次移动构造和析构,这是因为这个函数违背了RVO标准原则:

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function’s return value
在具有类返回类型的函数的 return 语句中,当表达式是具有与函数返回类型相同的 cv 非限定类型的非易失性自动对象(函数或 catch 子句参数除外)的名称时,通过将自动对象直接构造到函数的返回值中,可以省略复制/移动操作

也就是说,我们必须保证返回值语句的返回类型必须和函数的返回类型相同,。

通过上面std::move函数的定义,可以发现它的返回值是std::remove_reference<_Tp>::type&&,也就是Obj&&,与test_move函数返回值Obj不同,因此没有触发RVO。

因此我们再将函数test_move的返回值修改为Obj&&,再次执行输出为:

*** 3 ***
Obj()
~Obj()
Obj(Obj &&)

注意这里的输出顺序和上面的第一种方式不一样,很明显编译器执行了RVO优化。

当然,实际开发中不应采用这种方式,这是对本地局部对象的引用,仅仅只是为了演示效果。

4,std::move+RVO+std::string

回到引言中介绍的这个例子,我们以vs2013 release版为例,该环境下,编译器所采用的是NRVO优化策略,这会导致std::string分别被调用一次默认构造函数,以及移动构造函数。

而不使用std::move(ret),仅依靠编译器的优化,则只会调用一次默认构造函数,,错误的用法导致多调用了一次移动构造函数 + 析构函数,这会带来很大的额外开销嘛?

我们看一下vs2013实现的std::string源码:(仅截取部分)

//移动构造函数
basic_string(_Myt&& _Right) _NOEXCEPT
        : _Mybase(_Right._Getal())
        {   // construct by moving _Right
        _Tidy();
        _Assign_rv(_STD forward<_Myt>(_Right));
        }
 
//实际是调用_Assign_rv
void _Assign_rv(_Myt&& _Right)
        {   // assign by moving _Right
        if (_Right._Myres < this->_BUF_SIZE)
            _Traits::move(this->_Bx._Buf, _Right._Bx._Buf,
                _Right._Mysize + 1);
        else
            {   // copy pointer
            this->_Getal().construct(&this->_Bx._Ptr, _Right._Bx._Ptr);
            _Right._Bx._Ptr = pointer();
            }
        this->_Mysize = _Right._Mysize;
        this->_Myres = _Right._Myres;
        _Right._Tidy();
        }

可以很明显看到vs2013中的std::string做了SSO优化,if分支就是简单的栈内存拷贝,不涉及到额外开销;

我们主要看else分支,对于长度大于_BUF_SIZE的字符串,会进行一次指针拷贝(第17行代码):

//第一步:this->_Getal().construct
template<class _Ty,
        class... _Types>
        void construct(_Ty *_Ptr,
            _Types&&... _Args)
        {   // construct _Ty(_Types...) at _Ptr
        _Mytraits::construct(*this, _Ptr,
            _STD forward<_Types>(_Args)...);
        }
 
//第二步:_Mytraits::construct
template<class _Objty,
        class... _Types>
        static void construct(_Alloc& _Al, _Objty *_Ptr,
            _Types&&... _Args)
        {   // construct _Objty(_Types...) at _Ptr
        _Al.construct(_Ptr, _STD forward<_Types>(_Args)...);
        }
 
//第三步:_Al.construct
template<class _Objty,
        class... _Types>
        void construct(_Objty *_Ptr, _Types&&... _Args)
        {   // construct _Objty(_Types...) at _Ptr
        ::new ((void *)_Ptr) _Objty(_STD forward<_Types>(_Args)...);
        }

注意一下第三步中的new语法,它和我们常见的new操作并不相同,它只是简单的将第二个指针拷贝给第一个参数,并不涉及到内存malloc;

因此最终this→_Bx._Ptr和_Right._Bx._Ptr指向相同的地址,完成了移动构造。

因此,在编译器开启NRVO优化时,多调用一次std::move,对于std::string并不会带来较大的额外开销,但是还是可以避免调用的。

5,总结

1,目前的常用c++编译器都支持NRVO,C++11也已经把“允许编译器进行NRVO”写入了标准。经过测试,gcc编译器在debug和release模式下均支持NRVO,VS在debug模式下不支持NRVO,仅支持RVO,而在release模式下也支持NRVO;

2,std::move只是一个右值转换,它的代价低于拷贝构造,但是高于RVO,如果本地对象有资格获得RVO,就不要使用std::move;

3,对于复杂的函数,比如if分支返回函数值的情况,编译器无法使用RVO优化,此时可以考虑引入std::move。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-07 22:27:40  更:2022-04-07 22:28:17 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 0:03:26-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码