420 likes | 550 Views
更快的 C ++: Move 构造函数和完美转发. Pete Isensee 高级技术小组 微软. 问题陈述. 对对象的深拷贝是昂贵的 C++ 是建立在复制语义的基础之上的 STL 容器是以存值方式来进行储存 编译器临时对象是通过值拷贝来进行复制的 拷贝在源代码中常常是不明显的 游戏拷贝对象 – 很多!. 示例. 深拷贝. struct Texture { unsigned long mSize ; unsigned long* mpBits ; };. 浅拷贝. 更深的拷贝. struct Particle {
E N D
更快的C++:Move构造函数和完美转发 Pete Isensee 高级技术小组 微软
问题陈述 • 对对象的深拷贝是昂贵的 • C++是建立在复制语义的基础之上的 • STL容器是以存值方式来进行储存 • 编译器临时对象是通过值拷贝来进行复制的 • 拷贝在源代码中常常是不明显的 • 游戏拷贝对象 – 很多!
示例 深拷贝 struct Texture { unsigned long mSize; unsigned long* mpBits; }; 浅拷贝 更深的拷贝 struct Particle { Vector3 mPos; Vector3 mVel; Color mCol; }; structParticleSystem { std::vector< Particle > mPar; Texture mTex; }; ParticleSystemparticleSys(...); particleSys = StartExplosion(); // Explosion begins particleSys += AddSmoke(); // More particles added
ParticleSystemparticleSys(...); particleSys = StartExplosion(); // Explosion begins particleSys += AddSmoke(); // More particles added
ParticleSystemparticleSys(...); particleSys = StartExplosion(); // Explosion begins particleSys += AddSmoke(); // More particles added 粒子系统 开始爆炸() v v v v v v … … … … … t … … t t t t t v t v t …
复制临时对象是昂贵的 运算符性能=(常量粒子系统类)
避开临时对象是困难的 bool Connect( conststd::string& server, ... ); if( Connect( “microsoft.com” ) ) // temporary object created v.push_back( X(...) ); // another temporary a = b + c; // b + c is a temporary object x++; // returns a temporary object a = b + c + d; // c+d is a temporary object // b+(c+d) is another temporary object
我们所希望的是… • 这样的一种编译环境… • 我们可以避免不必要的复制 • 在此种情况下避免复制是安全的 • 完全由程序员进行控制 • 比如…
ParticleSystemparticleSys(...); particleSys = StartExplosion(); // Explosion begins particleSys += AddSmoke(); // More particles added 粒子系统 开始爆炸() v v v v … … … t … t t t v v t t … v v t t
考虑赋值 structParticleSystem { std::vector< Particle > mPar; Texture mTex; }; 典型的拷贝赋值 我们所想要的 ParticleSystem& operator=( constParticleSystem& rhs ) { if( this != &rhs ) { mPar = rhs.mPar; // Vector assignment (copy) mTex= rhs.mTex; // Texture assignment (copy) } return *this; } ParticleSystem& operator=( <Magic Type>rhs ) { // move semantics here ... return *this; }
解决方案:使用C++11标准来解决 • 在不需要的时候不要进行复制,相反的使用move语义来代替 • 深对象是极其重要的 • 主要的新语言特性:右值引用 • 启用move语义,包括 • Move构造 • Move赋值 • 完美转发
示例 拷贝赋值 Move赋值 ParticleSystem& operator=( constParticleSystem& rhs ) { if( this != &rhs ) { mPar = rhs.mPar; // Particle vector copy mTex= rhs.mTex; // Texture copy } return *this; } ParticleSystem& operator=( ParticleSystem&& rhs ) { if( this != &rhs ) { mPar = std::move( rhs.mPar ); // Vector move mTex= std::move( rhs.mTex ); // Texture move } return *this; }
可移动对象:右值 • Move来自 = 自我抽取 • 每一个表达不是左值就是右值 • 从一个右值进行移动总是安全的
左值与右值示例 ++x; // lvalue int a; // a is an lvalue X x; // x is an lvalue x++; // rvalue X(); // X() is an rvalue *ptr// lvalue int a = 1+2; // a is an lvalue; 1+2 is an rvalue foo( x ); // x is an lvalue x+42 // rvalue “abc” // lvalue foo( bar() ); // bar() is an rvalue 4321 // rvalue std::string( “abc” ) // rvalue
右值引用 T&& • T&: 引用(前C++11) • T&: C++11中的左值引用 • T&&: 右值引用;C++11中的新内容 • 右值引用所指向的对象可以安全地使用move语义 • 右值引用绑定至右值表达 • 左值引用绑定至左值表达
绑定 foo( ParticleSystem&& ); // A: rvalue foo( constParticleSystem&& ); // B: constrvalue foo( ParticleSystem& ); // C: lvalue foo( constParticleSystem& ); // D: constlvalue ParticleSystemparticleSys; constParticleSystemcparticleSys; foo( particleSys ); // lvalue foo( StartExplosion() ); // rvalue foo( cparticleSys ); // constlvalue
std::move ParticleSystem& operator=( ParticleSystem&& rhs ) { if( this != &rhs ) { mPar = std::move( rhs.mPar ); // Vector move assignment mTex= std::move( rhs.mTex ); // Texture move assignment } return *this; } • std::move ~= static_cast< T&& >(t) • 这等于告诉编译器:将该命名变量作为右值 • 由于引用崩溃、参数演绎和其他晦涩难懂的语言规则使得该函数的实现高度复杂 template< class T > inline typenamestd::remove_reference<T>::type&& move( T&& t ) noexcept { using ReturnType = typenamestd::remove_reference<T>::type&&; return static_cast< ReturnType >( t ); }
Move赋值 ParticleSystem& operator=( ParticleSystem&& rhs ) { if( this != &rhs ) { mPar = std::move( rhs.mPar ); // Vector move assignment mTex= std::move( rhs.mTex ); // Texture move assignment } return *this; } std::vector<T>& operator=( std::vector<T>&& rhs ) { if( this != &rhs ) { DestroyRange( mpFirst, mpLast ); // call all dtors if( mpFirst != nullptr ) free( mpFirst ); mpFirst = rhs.mpFirst; // eviscerate mpLast = rhs.mpLast; mpEnd = rhs.mpEnd; // rhs now empty shell rhs.mpFirst = rhs.mpLast = rhs.mpEnd = nullptr; } return *this; } // Standard assignment operator Texture& Texture::operator=( const Texture& rhs ) { if( this != &rhs ) { if( mpBits != nullptr) free( mpBits ); mSize = rhs.mSize; mpBits = malloc( mSize ); memcpy( mpBits, rhs.mpBits, mSize ); } return *this; } Texture& Texture::operator=( Texture&& rhs ) { if( this != &rhs ) { if( mpBits != nullptr ) free( mpBits ); mpBits = rhs.mpBits; // eviscerate mSize = rhs.mSize; rhs.mpBits = nullptr; // clear rhs } return *this; }
中场回顾 • 使用右值引用语义来启动moves • 使用非常量右值:重载运算符右边操作数rhs • std::move函数告诉编译器:“这是一个真正的右值。” • 绑定规则允许逐步转换 • 当你进行运算时实现右值引用 • 从低级程序库开始 • 或者从高层级的代码开始,由你自行决定
重新观察运算性能 运算符=(常量粒子系统类) 运算符=(粒子系统类)
Move构造函数 vector<T>::vector( vector<T>&& rhs ) : mpFirst( rhs.mpFirst ), // eviscerate mpLast ( rhs.mpLast), mpEnd ( rhs.mpEnd) { // rhs now an empty shell rhs.mpFirst = rhs.mpLast = rhs.mpEnd = nullptr; } Texture::Texture( Texture&& rhs ) : mpBits( rhs.mpBits ), // eviscerate mSize( rhs.mSize ) { // rhs now an empty shell rhs.mpBits = nullptr; } ParticleSystem::ParticleSystem( ParticleSystem&& rhs ) : // invoke member move ctors mPar( std::move( rhs.mPar ) ), mTex( std::move( rhs.mTex ) ) { }
完美转发问题 假设我们有一些setter函数 void ParticleSystem::SetTexture( const Texture& texture ) { mTex = texture; // We’d like to move if tx is a temporary } void ParticleSystem::SetTexture( Texture&& texture ) { mTex = std::move( texture ); // Move } void ParticleSystem::Set( constA& a, const B& b ) { // Uh-oh, we need three new overloads... }
使用函数模板及右值来进行解决 C++11中强大的新规则。鉴于: 模板右值引用参数可绑定到任意值 template< typename T > void f( T&& t ); // template function
绑定右值引用模板参数 示例 template< typename T > void f( T&& t ); // template function int a; constintca = 42; f( a ); // instantiates f( int& ); f( ca ); // instantiates f( constint& ); f( StartExplosion() ); // instantiates f( ParticleSystem&& );
完美转发 template< typename T > void ParticleSystem::SetTexture( T&& texture ) { mTex = std::forward<T>( texture ); // invokes right overload } std::forward<T> 相当于 • static_cast<[const] T&&>(t) 当 t 是一个右值 • static_cast<[const] T&>(t) 当 t 是一个左值 template< class T > inline T&& // typical std::forward implementation forward( typename identity<T>::type& t ) noexcept { return static_cast<T&&>( t ); }
完美的构造函数 典型的多参数构造函数;不处理右值 ParticleSystem::ParticleSystem( conststd::vector<Particle>& par, const Texture& texture ) : mPar( par ), mTex( texture ) { } 完美的构造函数;处理你往其中扔进的一切代码 template< typename V, typename T > ParticleSystem::ParticleSystem( V&& par, T&& texture ) : mPar( std::forward<V>( par ) ), mTex( std::forward<T>( texture ) ) { }
特殊的隐式成员函数 • 三法则(Rule of Three):如果你定义了三个成员函数的任意一个,你必须同时定义其他两个 • Move二法则(Rule of Two Moves):如果你定义了任意一个move函数,你必须同时定义另一个
明确特殊隐式函数 structParticleSystem { std::vector< Particle > mPar; // Copyable/movable object Texture mTex; // Copyable/movable object // Ctors ParticleSystem() = delete; ParticleSystem( constParticleSystem& ) = default; ParticleSystem( ParticleSystem&& ) = default; // Assign ParticleSystem& operator=( constParticleSystem& ) = default; ParticleSystem& operator=( ParticleSystem&& ) = default; // Destruction ~ParticleSystem() = default; };
C++11 template< typename T > swap( T& a, T& b ) { T tmp( std::move( a ) ); a = std::move( b ); b = std::move( tmp ); } • STL容器move启用 • 包括std::string • STL算法move启用 • 包括排序、分区、交换 • 只要通过简单的重新编译你就可以立即获得速度优势
推荐用语: 可移动类型 struct Deep { Deep( const Deep& ); // Copy ctor Deep( Deep&& ); // Move ctor template< typename A, typename B > Deep( A&&, B&& ); // Perfect forwarding ctor Deep& operator=( const Deep& ); // Copy assignment Deep& operator=( Deep&& ); // Move assignment ~Deep(); template< typename A > // Deep setters void SetA( A&& ); };
推荐用语:空指针 T( T&& rhs ) : ptr( rhs.ptr ) // eviscerate { rhs.ptr = nullptr; // rhs: safe state } Move构造函数 T& operator=( T&& rhs ) { if( this != &rhs ) { if( ptr != nullptr ) free( ptr ); ptr = rhs.ptr; // eviscerate rhs.ptr = nullptr; // rhs: safe state } return *this; } Move赋值
推荐用语:高级Objs T( T&& rhs ) : base( std::move( rhs ) ), // base m ( std::move( rhs.m ) ) // members { } Move构造函数 T& operator=( T&& rhs ) { if( this != &rhs ) { m = std::move( rhs.m ); // eviscerate } return *this; } Move赋值
推荐用法:完美转发 template< typename A, typename B > T( A&& a, B&& b ) : // binds to any 2 params ma( std::forward<A>( a ) ), mb( std::forward<B>( b ) ) { } 构造函数 template< typename A > void SetA( A&& a ) // binds to anything { ma = std::forward<A>( a ); } Setter函数
编译器和move支持 详尽清单:http://wiki.apache.org/stdcxx/C++0xCompilerSupport
进阶要点 • 通过重载右值引用,你可以在编译时选择是否跳转至x可移动的情况(x为临时对象) • 你可以逐步地实现超载 • 好处会累积至深对象 • 可显著提高性能
更进一步的研究:一些本次演讲所未涵盖的主题更进一步的研究:一些本次演讲所未涵盖的主题 • x值xvalues、泛左值glvalues、纯右值prvalues • 安置(例如“位置插入”) • 使用容器创建元素,w/ no moves/copies • 使用完美转发和可变参数函数 • 在其他情况下移动左值是OK的 • Moves和例外情况 • 完美转发并不总是那么完美 • 例如积分和指针类型;还有位域 • Noexcept和隐式move
最佳的做法 • 更新至支持右值引用的编译器 • 现在返回值是合理的了 – 既是可读的又是快速的 • 对深对象添加move构造函数/赋值/setters函数 • Move用语:this指针= 右指针rhspointers = null • 使用非常量右值引用 • 进行移动时,满足调用的obj不变量 • 避免返回常量T – 禁止move语义 • 明确特殊隐式函数 • 通过执行新的move代码以确保正确性
谢谢! • 我的联系方式: pkisensee@msn.com • 个人主页: http://www.tantalon.com/pete.htm • Scott Meyers: http://www.aristeia.com • Stephan Lavavej: http://blogs.msdn.com • Dave Abrahams: http://cpp-next.com • Thomas Becker: http://thbecker.net • Marc Gregoire: http://www.nuonsoft.com • 请让我知道你使用move语义启用你的代码后你所观察到的是什么样的结果
C++ 标准参考文献 • N1610 (v0.1) 2004 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2004/n1610.html • N2118 (v1.0) 2006 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2118.html • N2844 (v2.0) 2009 (VC10 impl) http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2009/n2844.html • N3310 (sections 840, 847, 858) (v2.1) 2011 (VC11 impl) http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html • N3053 (v3.0) 2010 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3053.html
右值参考文献 • Scott Meyers的Move语义和右值引用:http://www.aristeia.com/TalkNotes/ACCU2011_MoveSemantics.pdf以及http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references • Scott Meyers对完美转发的研究(C++ 及2011后版本) • Thomas Becker对右值引用的解释:http://thbecker.net/articles/rvalue_references/section_01.html • STL博客: http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx?PageIndex=3 • Marc Gregoire的博客http://www.nuonsoft.com/blog/2009/06/07/the-move-constructor-in-visual-c-2010/ • Visual Studio C++ 11的新特性http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx • MikaelKilpelainen的左值和右值http://accu.org/index.php/journals/227 • 从左值进行移动http://cpp-next.com/archive/2009/09/move-it-with-rvalue-references • 二进制运算符http://cpp-next.com/archive/2009/09/making-your-next-move/ • 安置http://stackoverflow.com/questions/4303513/push-back-vs-emplace-back