430 likes | 632 Vues
Faster C++: Move Construction and Perfect Forwarding. Pete Isensee Advanced Technology Group Microsoft. Problem Statement. Copying deep objects is expensive C++ is built on copy semantics STL containers store by value Compiler temporaries are copied by value
E N D
Faster C++:Move Construction and Perfect Forwarding Pete Isensee Advanced Technology Group Microsoft
Problem Statement • Copying deep objects is expensive • C++ is built on copy semantics • STL containers store by value • Compiler temporaries are copied by value • Copying is often non-obvious in source code • Games copy objects – a lot!
Example Deep struct Texture { unsigned long mSize; unsigned long* mpBits; }; Shallow Deeper 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 particleSys StartExplosion() v v v v v v … … … … … t … … t t t t t v t v t …
Copying Temp Objects is Expensive Perfof operator=(constParticleSystem&)
Avoiding Temporaries is Difficult 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
What We Would Like… • Is a world where… • We could avoid unnecessary copies • In cases where it was safe to do so • Completely under programmer control • For example…
ParticleSystemparticleSys(...); particleSys = StartExplosion(); // Explosion begins particleSys += AddSmoke(); // More particles added particleSys StartExplosion() v v v v … … … t … t t t v v t t … v v t t
Consider Assignment structParticleSystem { std::vector< Particle > mPar; Texture mTex; }; Canonical copy assignment What we want 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; }
Solution: C++11 Standard to the Rescue • Don’t copy when you don’t need to; move instead • Critically important for deep objects • Key new language feature: rvalue references • Enables move semantics, including • Move construction • Move assignment • Perfect forwarding
Example Copy assignment Move assignment 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; }
Movable Objects: rvalues • Move from = eviscerate thyself • Every expression is either an lvalue or rvalue • Always safe to move from an rvalue
lvalue/rvalue examples ++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
rvalue References T&& • T&: reference (pre C++11) • T&: lvalue reference in C++11 • T&&: rvalue reference; new in C++11 • rvalue references indicate objects that can be safely moved from • rvalue references bind to rvalue expressions • lvalue references bind to lvalue expressions
Binding 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) • Tells compiler: treat this named variable as an rvalue • Highly complex implementation due to reference collapsing, parameter deduction and other arcane language rules 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 assign 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; }
Intermission • Use rvalue reference semantics to enable moves • Use non-constrvalue: rhs is reset • std::move tells compiler “this is really an rvalue” • Binding rules allow gradual conversion • Implement rvalue reference semantics as you go • Start in low-level libraries • Or start in high-level code, your choice
Performance Revisited operator=(constParticleSystem&) operator=(ParticleSystem&&)
Move Constructors 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 ) ) { }
Perfect Forwarding Problem Suppose we have some setter functions 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... }
Func Templates Plus rvalues to the Rescue Powerful new rule in C++11. Given: Template rvalue ref param binds to anything template< typename T > void f( T&& t ); // template function
Binding rvalue Reference Template Params Examples 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&& );
Perfect Forwarding template< typename T > void ParticleSystem::SetTexture( T&& texture ) { mTex = std::forward<T>( texture ); // invokes right overload } std::forward<T> equivalent to • static_cast<[const] T&&>(t) when t is an rvalue • static_cast<[const] T&>(t) when t is an lvalue template< class T > inline T&& // typical std::forward implementation forward( typename identity<T>::type& t ) noexcept { return static_cast<T&&>( t ); }
Perfect Constructors Typical multi-argctor; doesn’t handle rvalues ParticleSystem::ParticleSystem( conststd::vector<Particle>& par, const Texture& texture ) : mPar( par ), mTex( texture ) { } Perfect constructor; handles everything you throw at it! template< typename V, typename T > ParticleSystem::ParticleSystem( V&& par, T&& texture ) : mPar( std::forward<V>( par ) ), mTex( std::forward<T>( texture ) ) { }
Implicit Special Member Functions • Rule of Three: if you define any of the first three, define all • Rule of Two Moves: If you define either move, define both
Be Explicit About Implicit Special Functions 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 containers move enabled • Including std::string • STL algorithms move enabled • Including sort, partition, swap • You get immediate speed advantages simply by recompiling
Recommended Idioms: Moveable Types 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&& ); };
Recommended Idioms: Raw Pointers T( T&& rhs ) : ptr( rhs.ptr ) // eviscerate { rhs.ptr = nullptr; // rhs: safe state } Move ctor 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 assignment
Recommended Idioms: Higher Level Objs T( T&& rhs ) : base( std::move( rhs ) ), // base m ( std::move( rhs.m ) ) // members { } Move ctor T& operator=( T&& rhs ) { if( this != &rhs ) { m = std::move( rhs.m ); // eviscerate } return *this; } Move assignment
Recommended Idioms: Perfect Forwarding 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 ) ) { } Ctor template< typename A > void SetA( A&& a ) // binds to anything { ma = std::forward<A>( a ); } Setter
Compilers and Move Support Exhaustive list: http://wiki.apache.org/stdcxx/C++0xCompilerSupport
High Level Takeaways • By overloading on rvalue references, you can branch at compile time on the condition that x is moveable (a temporary object) or not • You can implement the overloading gradually • Benefits accrue to deep objects • Performance improvements can be significant
Further Research: Topics I Didn’t Cover • xvalues, glvalues, prvalues • Emplacement (e.g. “placement insertion”) • Create element within container, w/ no moves/copies • Uses perfect forwarding and variadic functions • Other scenarios where moving lvalues is OK • Moves and exceptions • Perfect forwarding not always so perfect • e.g. integral and pointer types; bitfields, too • noexcept and implicit move
Best Practices • Update to compilers that support rvalue references • Return by value is now reasonable – both readable and fast • Add move ctor/assignment/setters to deep objects • Move idiom: this = rhs pointers, rhs pointers = null • Use non-constrvalue references • When moving, satisfy moved-from obj invariants • Avoid return by const T – prevents move semantics • Be explicit about implicit special functions • Step thru new move code to ensure correctness
Thanks! • Contact me: pkisensee@msn.com • Slides: 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 • Let me know what kind of results you see when you move enable your code
C++ Standard References • 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
rvalueReferences • Scott Meyers’ Move Semantics and rvalueReferences: http://www.aristeia.com/TalkNotes/ACCU2011_MoveSemantics.pdf and http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references • Scott Meyers’ Adventures in Perfect Forwarding (C++ and Beyond 2011) • Thomas Becker’s rvalueReferences Explained: http://thbecker.net/articles/rvalue_references/section_01.html • STL’s blog: http://blogs.msdn.com/b/vcblog/archive/2009/02/03/rvalue-references-c-0x-features-in-vc10-part-2.aspx?PageIndex=3 • Marc Gregoire’s Blog http://www.nuonsoft.com/blog/2009/06/07/the-move-constructor-in-visual-c-2010/ • C++11 Features in Visual Studio C++ 11 http://blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx • Mikael Kilpelainen’sLvalues and Rvalueshttp://accu.org/index.php/journals/227 • Moving from lvalueshttp://cpp-next.com/archive/2009/09/move-it-with-rvalue-references • Binary operators http://cpp-next.com/archive/2009/09/making-your-next-move/ • Emplacement http://stackoverflow.com/questions/4303513/push-back-vs-emplace-back