Visual studio VS2013:优化具有向量成员的类的移动语义的潜在问题?

Visual studio VS2013:优化具有向量成员的类的移动语义的潜在问题?,visual-studio,c++11,vector,visual-studio-2013,move-semantics,Visual Studio,C++11,Vector,Visual Studio 2013,Move Semantics,我在VS2013上编译了以下代码(使用“发布”模式优化),并沮丧地发现std::swap(v1,v2)的程序集与std::swap(v3,v4)不同 #包括 #包括 #包括 模板 类包装向量 { 公众: 类型定义T值_类型; void push_back(T值){m_vec.push_back(值);} WRAPPED_VEC()=默认值; WRAPPED_-VEC(WRAPPED_-VEC&&other):m_-VEC(std::move(other.m_-VEC)){} 包裹的向量和运算符=

我在VS2013上编译了以下代码(使用“发布”模式优化),并沮丧地发现
std::swap(v1,v2)
的程序集与
std::swap(v3,v4)
不同

#包括
#包括
#包括
模板
类包装向量
{
公众:
类型定义T值_类型;
void push_back(T值){m_vec.push_back(值);}
WRAPPED_VEC()=默认值;
WRAPPED_-VEC(WRAPPED_-VEC&&other):m_-VEC(std::move(other.m_-VEC)){}
包裹的向量和运算符=(包裹的向量和其他)
{
m_vec=std::move(other.m_vec);
归还*这个;
}
私人:
std::向量m_-vec;
};
int main(int,char*[])
{
包裹向量v1、v2;
std::generate_n(std::back_inserter(v1),10,std::rand);
std::generate_n(std::back_inserter(v2),10,std::rand);
标准:交换(v1,v2);
std::向量v3,v4;
std::generate_n(std::back_inserter(v3),10,std::rand);
std::generate_n(std::back_inserter(v4),10,std::rand);
标准:交换(v3,v4);
返回0;
}

std::swap(v3,v4)语句将变成“完美”程序集。我如何才能在
std::swap(v1,v2)
中实现相同的效率?

原因是
std::swap
对类型
std::vector
有一个优化的重载(请参阅右键单击->转到定义)。要使此代码在包装器中快速运行,请按照关于std::swap的说明进行操作:

对于用户定义的类型,swap可以在命名空间std中专门化, 但是ADL找不到这样的专门化(名称空间std是 不是用户定义类型的关联命名空间)。预期的 使用户定义的类型可交换的方法是提供非成员 与类型位于同一命名空间中的函数交换:有关详细信息,请参阅 细节


原因是
std::swap
确实有一个针对类型
std::vector
的优化重载(请参阅右键单击->转到定义)。要使此代码在包装器中快速运行,请按照关于std::swap的说明进行操作:

对于用户定义的类型,swap可以在命名空间std中专门化, 但是ADL找不到这样的专门化(名称空间std是 不是用户定义类型的关联命名空间)。预期的 使用户定义的类型可交换的方法是提供非成员 与类型位于同一命名空间中的函数交换:有关详细信息,请参阅 细节


这里有几点需要说明

1.如果您不能绝对肯定您调用
swap
的方式等同于调用
swap
的“正确”方式,则应始终使用“正确”方式:

2.查看程序集以查找调用
swap
之类内容的一种非常方便的方法是将调用本身放入测试函数中。这样可以很容易地隔离部件:

void
test1(WRAPPED_VEC<int>& v1, WRAPPED_VEC<int>& v2)
{
    using std::swap;
    swap(v1, v2);
}

void
test2(std::vector<int>& v1, std::vector<int>& v2)
{
    using std::swap;
    swap(v1, v2);
}
这很快。它将使用
WRAPPED\u VEC
的移动构造函数和移动赋值运算符

然而
vector
交换速度更快:它交换
vector
的3个指针,如果
std::allocator\u traits::propagate\u on\u container\u swap::value
为真(而不是真),也交换分配器。如果它为false(并且是),并且如果两个分配器相等(并且它们相等),那么一切都正常。否则会发生未定义的行为

要使
test1
test2
在性能方面相同,您需要:

friend
void
swap(WRAPPED_VEC<int>& v1, WRAPPED_VEC<int>& v2)
{
    using std::swap;
    swap(v1.m_vec, v2.m_vec);
}
朋友
无效的
交换(已包装的向量&v1,已包装的向量&v2)
{
使用std::swap;
交换(v1.m_vec,v2.m_vec);
}
有一件有趣的事情需要指出:

在您的情况下,如果总是使用
std::allocator
,那么
friend
函数总是一个胜利。但是,如果您的代码允许其他分配器,可能是那些具有状态的分配器,它们可能比较不相等,并且可能具有
propagate\u on\u container\u swap::value
false(正如
std::allocator
所做的那样),那么
swap
for
WRAPPED\u VEC
的这两种实现会有所不同:

1.如果您依赖于
std::swap
,那么您会受到性能的影响,但您永远不会有可能进入未定义的行为。在
向量上移动构造始终是定义良好的O(1)。向量上的移动赋值总是定义良好的,可以是O(1)或O(N),也可以是noexcept(true)或noexcept(false)

如果
propagate\u on\u container\u move\u assignment::value
为false,并且如果移动分配中涉及的两个分配器不相等,
vector
move assignment将变为O(N)和noexcept(false)。因此,使用
vector
move赋值的
swap
将继承这些特征。然而,无论发生什么,行为总是很明确的

2.如果您为
WRAPPED_VEC
重载
swap
,从而依赖
swap
vector
重载,那么如果分配器比较不相等,并且
在容器上传播\u swap::value
等于false,那么您将面临未定义行为的可能性。但你会获得一个潜在的绩效胜利

和往常一样,需要进行工程权衡。这篇文章旨在提醒你这些权衡的本质


附:下面的评论纯粹是文体上的。类类型的所有大写名称通常被认为是糟糕的样式。传统上,所有大写名称都是为宏保留的。

这里有几点需要说明

1.如果您不能绝对肯定您调用
swap
的方式等同于调用
swap
的“正确”方式,则应始终使用“正确”方式:

2.这是查看组件的一种非常方便的方法
void
test1(WRAPPED_VEC<int>& v1, WRAPPED_VEC<int>& v2)
{
    using std::swap;
    swap(v1, v2);
}

void
test2(std::vector<int>& v1, std::vector<int>& v2)
{
    using std::swap;
    swap(v1, v2);
}
template <class T>
inline
swap(T& x, T& y) noexcept(is_nothrow_move_constructible<T>::value &&
                          is_nothrow_move_assignable<T>::value)
{
    T t(std::move(x));
    x = std::move(y);
    y = std::move(t);
}
friend
void
swap(WRAPPED_VEC<int>& v1, WRAPPED_VEC<int>& v2)
{
    using std::swap;
    swap(v1.m_vec, v2.m_vec);
}