繁体   English   中英

移动分配比复制分配慢 - 错误,功能还是未指定?

[英]Move-assignment slower than copy-assignment — bug, feature, or unspecified?

我最近意识到在C ++ 11中添加移动语义(或者至少我的实现,Visual C ++)已经积极地(并且非常显着地) 打破了我的一个优化。

请考虑以下代码:

#include <vector>
int main()
{
    typedef std::vector<std::vector<int> > LookupTable;
    LookupTable values(100);  // make a new table
    values[0].push_back(1);   // populate some entries

    // Now clear the table but keep its buffers allocated for later use
    values = LookupTable(values.size());

    return values[0].capacity();
}

我遵循这种模式来执行容器回收 :我将重新使用相同的容器而不是销毁和重新创建它,以避免不必要的堆释放和(立即)重新分配。

在C ++ 03上,这很好 - 这意味着这个代码用于返回1 ,因为向量是按元素复制的 ,而它们的底层缓冲区保持原样。 因此,我可以修改每个内部向量,因为它知道它可以使用与之前相同的缓冲区。

然而,在C ++ 11上,我注意到这会导致右侧移动到左侧,这对左侧的每个向量执行元素移动分配。 这反过来导致向量丢弃其旧缓冲区,突然将其容量减少到零。 因此,由于堆分配/释放过多,我的应用程序现在会大大减慢速度。

我的问题是:这种行为是一个错误,还是故意的? 是否甚至由标准指定?

更新:

我刚刚意识到这种特定行为的正确性可能取决于a = A()可以使指向a的元素的迭代器无效。 但是,我不知道移动赋值的迭代器失效规则是什么,所以如果你知道它们,你的答案中可能值得一提。

C ++ 11

C ++ 03和C ++ 11中OP的行为差异是由于移动分配的实现方式。 有两个主要选择:

  1. 摧毁LHS的所有元素。 取消分配LHS的底层存储空间。 将底层缓冲区(指针)从RHS移动到LHS。

  2. 从RHS的元素移动分配到LHS的元素。 如果RHS有更多,则消除LHS的任何多余元素或移动构建LHS中的新元素。

我认为如果移动不是noexcept,可以使用选项2和副本。

选项1使LHS的所有引用/指针/迭代器无效,并保留RHS的所有迭代器等。 它需要O(LHS.size())析构,但缓冲区移动本身是O(1)。

选项2仅使已销毁的LHS的多余元素的迭代器无效,或者如果发生LHS的重新分配则使所有迭代器无效。 它是O(LHS.size() + RHS.size())因为双方的所有元素都需要处理(复制或销毁)。

据我所知,无法保证C ++ 11中会出现哪一个(参见下一节)。

理论上,只要您可以在操作后使用存储在LHS中的分配器释放底层缓冲区,就可以使用选项1。 这可以通过两种方式实现:

  • 如果两个分配器比较相等,则可以使用一个分配器解除分配通过另一个分配的存储。 因此,如果LHS和RHS的分配器在移动之前比较相等,则可以使用选项1.这是运行时决策。

  • 如果可以将分配器从RHS 传播 (移动或复制)到LHS,则LHS中的这个新分配器可用于解除分配RHS的存储。 是否传播分配器由allocator_traits<your_allocator :: propagate_on_container_move_assignment确定。 这由类型属性决定,即编译时决定。


C ++ 11减去缺陷/ C ++ 1y

LWG 2321 (仍然开放)之后,我们保证:

容器(数组除外)的无移动构造函数(或者当allocator_traits<allocator_type> :: propagate_on_container_move_assignment :: valuetrue时移动赋值运算符)使引用源容器元素的任何引用,指针或迭代器无效。 [ 注意: end()迭代器不引用任何元素,因此它可能无效。 - 结束说明 ]

要求在移动赋值上传播的那些分配器的移动分配必须移动vector对象的指针,但不得移动矢量的元素。 (选项1)

LWG缺陷2103之后,默认分配器在容器的移动分配期间传播,因此禁止 OP中的技巧移动各个元素。


我的问题是:这种行为是一个错误,还是故意的? 是否甚至由标准指定?

不,是的,没有(可以说)。

有关vector移动分配必须如何工作的详细说明,请参阅此答案 当您使用std::allocator ,C ++ 11会将您置于案例2中,委员会中的许多人认为这是一个缺陷,并且已针对C ++ 14更正为案例1。

案例1和案例2都具有相同的运行时行为,但案例2对vector::value_type有额外的编译时要求。 情况1和情况2都导致在移动分配期间将内存所有权从rhs转移到lhs,从而为您提供观察到的结果。

这不是一个错误。 这是故意的。 它由C ++ 11和forward指定。 是的,正如他在回答中指出的那样,有一些小缺陷。 但是这些缺陷都不会改变你所看到的行为。

正如评论中指出的那样,最简单的解决方法是创建一个as_lvalue帮助器并使用它:

template <class T>
constexpr
inline
T const&
as_lvalue(T&& t)
{
    return t;
}

// ...

// Now clear the table but keep its buffers allocated for later use
values = as_lvalue(LookupTable(values.size()));

这是零成本,并使您精确地返回C ++ 03行为。 它可能不会通过代码审查。 你可以更清楚地迭代并clear外部向量中的每个元素:

// Now clear the table but keep its buffers allocated for later use
for (auto& v : values)
    v.clear();

后者是我推荐的。 前者是(imho)混淆的。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM