Upload
cedric-alston
View
95
Download
2
Embed Size (px)
DESCRIPTION
更快的 C ++: Move 构造函数和完美转发. Pete Isensee 高级技术小组 微软. 问题陈述. 对对象的深拷贝是昂贵的 C++ 是建立在复制语义的基础之上的 STL 容器是以存值方式来进行储存 编译器临时对象是通过值拷贝来进行复制的 拷贝在源代码中常常是不明显的 游戏拷贝对象 – 很多!. 示例. 深拷贝. struct Texture { unsigned long mSize ; unsigned long* mpBits ; };. 浅拷贝. 更深的拷贝. struct Particle { - PowerPoint PPT Presentation
Citation preview
更快的 C++:Move 构造函数和完美转发
Pete Isensee高级技术小组微软
问题陈述
• 对对象的深拷贝是昂贵的• C++ 是建立在复制语义的基础之上的– STL 容器是以存值方式来进行储存– 编译器临时对象是通过值拷贝来进行复制的
• 拷贝在源代码中常常是不明显的• 游戏拷贝对象 – 很多!
示例
struct ParticleSystem { std::vector< Particle > mPar; Texture mTex;};
更深的拷贝
ParticleSystem particleSys(...); particleSys = StartExplosion(); // Explosion beginsparticleSys += AddSmoke(); // More particles added
struct Texture { unsigned long mSize; unsigned long* mpBits;};
深拷贝
浅拷贝struct Particle { Vector3 mPos; Vector3 mVel; Color mCol;};
ParticleSystem particleSys(...); particleSys = StartExplosion(); // Explosion beginsparticleSys += AddSmoke(); // More particles added
ParticleSystem particleSys(...);particleSys = StartExplosion(); // Explosion beginsparticleSys += AddSmoke(); // More particles added
粒子系统 开始爆炸 ()
tv
t
…
v
t
…
v
t
…
v
tv
t
…
v
tv
…
…
…
t
…
v
复制临时对象是昂贵的
1 10 100 1000 10000 100000 10000001
10
100
1000
10000
100000
1000000
粒子
时间( Ticks)
粒子复制
( ticks )1 6,113
10 7,201100 5,543
1,000 8,57910,000 56,614
100,000 635,9621,000,000 6,220,013
运算符性能 = (常量 粒子系统类)
避开临时对象是困难的bool Connect( const std::string& server, ... );if( Connect( “microsoft.com” ) ) // temporary object created
v.push_back( X(...) ); // another temporary
a = b + c; // b + c is a temporary object
a = b + c + d; // c+d is a temporary object // b+(c+d) is another temporary object
x++; // returns a temporary object
我们所希望的是…
• 这样的一种编译环境…– 我们可以避免不必要的复制– 在此种情况下避免复制是安全的– 完全由程序员进行控制
• 比如…
ParticleSystem particleSys(...);particleSys = StartExplosion(); // Explosion beginsparticleSys += AddSmoke(); // More particles added
粒子系统 开始爆炸 ()
tv
tv
t
…
v
tv
tv
t
…
v
tv
…t
…
v
…
考虑赋值 struct ParticleSystem { std::vector< Particle > mPar; Texture mTex;};
ParticleSystem& operator=( const ParticleSystem& 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 赋值– 完美转发
示例 ParticleSystem& operator=( const ParticleSystem& 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 赋值
可移动对象:右值• Move 来自 = 自我抽取• 每一个表达不是左值就是右值• 从一个右值进行移动总是安全的
左值 右值
在内存中 是 否
可以获取地址 是 否
有名字 是 否
可移动 否 * 是
左值与右值示例
X x; // x is an lvalue
X(); // X() is an rvalue
int a; // a is an lvalue
int a = 1+2; // a is an lvalue; 1+2 is an rvalue
foo( x ); // x is an lvalue
foo( bar() ); // bar() is an rvalue
++x; // lvalue
“abc” // lvalue
*ptr // lvalue
4321 // rvalue
x+42 // rvalue
x++; // rvalue
std::string( “abc” ) // rvalue
右值引用
• T&: 引用(前 C++11 )• T&: C++11 中的左值引用• T&&: 右值引用; C++11 中的新内容• 右值引用所指向的对象可以安全地使用
move 语义• 右值引用绑定至右值表达• 左值引用绑定至左值表达
T&&
绑定foo( ParticleSystem&& ); // A: rvaluefoo( const ParticleSystem&& ); // B: const rvaluefoo( ParticleSystem& ); // C: lvaluefoo( const ParticleSystem& ); // D: const lvalue
ParticleSystem particleSys;const ParticleSystem cparticleSys;
foo( particleSys ); // lvaluefoo( StartExplosion() ); // rvaluefoo( cparticleSys ); // const lvalue
绑定和重载解析规则表达
引用类型
右值 常量右值const rvalue
左值 常量左值const ivalue
优先级
T&& 是 最高
const T&& 是 是
T& 是
const T& 是 是 是 是 最低
std::move
• std::move ~= static_cast< T&& >(t)• 这等于告诉编译器:将该命名变量作为右值• 由于引用崩溃、参数演绎和其他晦涩难懂的语言规则使得
该函数的实现高度复杂
template< class T > inline typename std::remove_reference<T>::type&&move( T&& t ) noexcept { using ReturnType = typename std::remove_reference<T>::type&&; return static_cast< ReturnType >( t );}
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; }
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 operatorTexture& 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 函数告诉编译器:“这是一个真正
的右值。”• 绑定规则允许逐步转换– 当你进行运算时实现右值引用– 从低级程序库开始– 或者从高层级的代码开始,由你自行决定
重新观察运算性能
粒子复制
(ticks)Move (ticks)
1 6,113 101910 7,201 1100
100 5,543 9681,000 8,579 1200
10,000 56,614 865100,000 635,962 993
1,000,000 6,220,013 1173
1 10 100 1000 10000 100000 10000001
10
100
1000
10000
100000
1000000
粒子
时间( Ticks)
运算符 = (常量 粒子系统类)
运算符 = (粒子系统类)
Move 构造函数ParticleSystem::ParticleSystem( ParticleSystem&& rhs ) : // invoke member move ctors mPar( std::move( rhs.mPar ) ), mTex( std::move( rhs.mTex ) ) {} 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; }
完美转发问题假设我们有一些 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( const A& a, const B& b ) { // Uh-oh, we need three new overloads...}
使用函数模板及右值来进行解决C++11 中强大的新规则。鉴于:
模板右值引用参数可绑定到任意值template< typename T > void f( T&& t ); // template function
表达引用类型 右值 常量右值 左值 常量左值 优先级
template T&& 是 是 是 是 最高T&& 是const T&& 是 是T& 是const T& 是 是 是 是 最低
绑定右值引用模板参数示例
template< typename T > void f( T&& t ); // template function
int a;const int ca = 42;
f( a ); // instantiates f( int& );f( ca ); // instantiates f( const int& );f( StartExplosion() ); // instantiates f( ParticleSystem&& );
完美转发template< typename T >void ParticleSystem::SetTexture( T&& texture ) { mTex = std::forward<T>( texture ); // invokes right overload}
template< class T > inline T&& // typical std::forward implementationforward( typename identity<T>::type& t ) noexcept { return static_cast<T&&>( t );}
std::forward<T> 相当于– static_cast<[const] T&&>(t) 当 t 是一个右值– static_cast<[const] T&>(t) 当 t 是一个左值
完美的构造函数ParticleSystem::ParticleSystem( const std::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 函数,你必须同时定义另一个
函数 隐式产生的情况Default ctor 没有其他的构造函数显式声明Copy ctor 没有 move 构造函数或 move 赋值显示声明Copy assign 没有 move 构造函数或 move 赋值显示声明Move ctor 没有拷贝构造函数、 move 赋值或析构函数显示声明Move assign 没有拷贝构造函数、拷贝赋值或析构函数显示声明Dtor 没有析构函数显示声明
明确特殊隐式函数struct ParticleSystem { std::vector< Particle > mPar; // Copyable/movable object Texture mTex; // Copyable/movable object
// Ctors ParticleSystem() = delete; ParticleSystem( const ParticleSystem& ) = default; ParticleSystem( ParticleSystem&& ) = default; // Assign ParticleSystem& operator=( const ParticleSystem& ) = default; ParticleSystem& operator=( ParticleSystem&& ) = default; // Destruction ~ParticleSystem() = default; };
C++11• STL 容器 move 启用– 包括 std::string
• STL 算法 move 启用– 包括排序、分区、交换
• 只要通过简单的重新编译你就可以立即获得速度优势
template< typename T >swap( T& a, T& b ) { T tmp( std::move( a ) ); a = std::move( b ); b = std::move( tmp );}
推荐用语 : 可移动类型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}
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 构造函数
Move 赋值
推荐用语:高级 ObjsT( T&& rhs ) : base( std::move( rhs ) ), // base m ( std::move( rhs.m ) ) // members{}
T& operator=( T&& rhs ) { if( this != &rhs ) { m = std::move( rhs.m ); // eviscerate } return *this;}
Move 构造函数
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支持特征 微软 GCC Intel Clang
右值引用 VS 2010 4.3 11.1 2.9
STL move 语义 VS 2010 4.3 11.1 2.9
空指针标识 nullptr VS 2010 4.6 12.1 2.9
可变参数模板 4.3 12.1 2.9
Defaulted 和 deleted 函数 4.4 12.0 3.0
noexcept 4.6 3.0
详尽清单: 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 指针 = 右指针 rhs pointers = null• 使用非常量右值引用• 进行移动时,满足调用的 obj 不变量• 避免返回常量 T – 禁止move 语义• 明确特殊隐式函数• 通过执行新的 move 代码以确保正确性
谢谢!• 我的联系方式 : [email protected] • 个人主页 : 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
• Mikael Kilpelainen 的左值和右值 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