【问题标题】:Are compilers clever enough to std::move variables going out of scope?编译器是否足够聪明地 std::move 变量超出范围?
【发布时间】:2018-06-16 00:24:33
【问题描述】:

考虑以下代码:

std::vector<int> Foo() {
    std::vector<int> v = Bar();
    return v;
}

return v 是 O(1),因为 NRVO 将省略副本,构造 v directly in the storage where the function's return value would otherwise be moved or copied to。现在考虑功能类似的代码:

void Foo(std::vector<int> * to_be_filled) {
    std::vector<int> v = Bar();
    *to_be_filled = v;
}

可以在这里提出类似的论点,因为*to_be_filled = v 可以被编译为 O(1) 移动分配,因为它是一个超出范围的局部变量(编译器应该很容易验证v 在这种情况下没有外部引用,因此在最后一次使用时将其提升为右值)。是这样吗?有什么微妙的原因吗?

此外,感觉这种模式可以扩展到左值超出范围的任何上下文:

void Foo(std::vector<int> * to_be_filled) {
  if (Baz()) {
    std::vector<int> v = Bar();
    *to_be_filled = v;
  }
  ...
}

期望编译器找到诸如*to_be_filled = v 之类的模式,然后自动优化它们以假定右值语义是否有用/合理?


编辑:

g++ 7.3.0 在 -O3 模式下执行任何此类优化。

【问题讨论】:

  • 我很确定标准不允许这种优化。但是,您可能会提交将其添加到标准中的提案(这意味着当 v 出现在作业右侧的唯一内容时,v 超出范围,而 v 不在't 在左侧),这样v 就会被移动,或者甚至可以省略副本
  • 你为什么不尝试使用用户定义的类来打印构造函数和析构函数中的消息,看看使用了什么。
  • @Justin:我一直在考虑提交一个接近此的提案,但想弄清楚我们可以制作这种模式的通用性,为它编写优化的难度/成本有多大, 与该模式在现实世界中的常见程度(即:我给出的示例很容易检测到,但可能不太常见)。
  • @Barmar:好点。我编辑了问题以包含 g++ 的结果。不出所料,这种优化似乎没有实现,至少在 -O3 中是这样。
  • @AndréHarder 查看std-proposals google 组。这就是我要从提案开始的地方(稍微充实我的想法,在那里提出)。请注意,这实际上是该语言的一个相当困难的角落。在每种情况下都有很多陷阱可以防止这种情况发生,但您可以针对某些特定情况提出建议。

标签: c++ compiler-optimization rvalue nrvo


【解决方案1】:

允许编译器任意决定将左值名称转换为要从中移动的右值。它只能在 C++ 标准允许的情况下这样做。例如在return 语句中(并且仅在其return &lt;identifier&gt;; 时)。

*to_be_filled = v; 将始终执行复制。即使它是可以访问v 的最后一条语句,它也始终是一个副本。不允许编译器更改它。

我的理解是返回 v 是 O(1),因为 NRVO 将(实际上)将 v 变成一个右值,然后使用 std::vector 的移动构造函数。

这不是它的工作原理。 NRVO 将完全消除移动/复制。但是return &lt;identifier&gt;; 成为右值的能力并不是“优化”。这实际上是编译器将它们视为右值的要求

编译器可以选择复制省略。编译器无法选择 return &lt;identifier&gt;; 的作用。因此,上述内容要么根本不移动(如果发生 NRVO),要么会移动对象。

为什么不这样做有一个微妙的原因吗?

不允许这样做的一个原因是,语句的位置不应随意更改该语句的作用。看,return &lt;identifier&gt;; 将始终从标识符移动(如果它是局部变量)。它在函数中的位置无关紧要。由于是return 语句,我们知道如果return 被执行,它之后的任何内容都不会被执行。

这不是任意陈述的情况。表达式*to_be_filled = v; 的行为不应根据它在代码中的位置而改变。你不应该仅仅因为你在函数中添加了另一行就可以将一个移动变成一个副本。

另一个原因是任意语句很快就会变得非常复杂。 return &lt;identifier&gt;; 很简单;它将标识符复制/移动到返回值并返回。

相比之下,如果您引用了v,而to_be_filled 以某种方式使用了它,会发生什么情况。当然这不会发生在您的情况下,但是其他更复杂的情况呢?可以想象,最后一个表达式可以从对已移动对象的引用中读取。

return &lt;identifier&gt;; 的情况下要做到这一点要困难得多。

【讨论】:

  • 如果 copy-ctor 没有做任何有趣的事情,他们可以使用 as-if 规则。它不必在调试器中是可观察的。
  • @o11c:如果复制移动操作是微不足道的(即:不可观察),那么它实际上并不重要;无论哪种方式,它都会做一个 memcpy。但通过一个不平凡的举动(如vector),这是可观察到的行为。
  • 只有当向量的 contents 的复制/移动很有趣时才能观察到。允许优化出new+memcpy+delete。
  • @o11c:分配是可观察的行为,这要归功于vectorAllocator 参数。如果是my_vector 类型而不是std::vector,则选择将是直接可观察的。此处不允许编译器调用移动构造函数。期间。
  • 对“所以上面的内容要么根本不移动(如果发生 NRVO)要么移动对象。” :如果对象有一个复制构造函数但没有移动构造函数,那么如果 NRVO 没有发生,它将复制该对象。标准将此称为“复制/移动”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-08-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-04-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多