27

MSDN 文章How to: Write a Move Constructor有以下建议。

如果为类同时提供移动构造函数和移动赋值运算符,则可以通过编写移动构造函数来调用移动赋值运算符来消除冗余代码。以下示例显示了调用移动赋值运算符的移动构造函数的修订版本:

// Move constructor.
MemoryBlock(MemoryBlock&& other)
   : _data(NULL)
   , _length(0)
{
   *this = std::move(other);
}

通过双重初始化MemoryBlock's 的值,这段代码效率低下,还是编译器能够优化掉额外的初始化?我是否应该总是通过调用移动赋值运算符来编写移动构造函数?

4

6 回答 6

15

[...] 编译器是否能够优化掉额外的初始化?

在几乎所有情况下:是的。

我是否应该总是通过调用移动赋值运算符来编写移动构造函数?

是的,只需通过移动赋值运算符实现它,除非在您测量它导致次优性能的情况下。


今天的优化器在优化代码方面做得非常出色。您的示例代码特别容易优化。首先:移动构造函数在几乎所有情况下都会被内联。如果您通过移动赋值运算符实现它,则该运算符也将被内联。

让我们看看一些组装!显示了来自 Microsoft 网站的确切代码,其中包含两个版本的移动构造函数:手动和通过移动赋值。以下是 GCC 的汇编输出-O-O1具有相同的输出;clang 的输出导致相同的结论):

; ===== manual version =====           |   ; ===== via move-assig =====
MemoryBlock(MemoryBlock&&):            |   MemoryBlock(MemoryBlock&&):
    mov     QWORD PTR [rdi], 0         |       mov     QWORD PTR [rdi], 0
    mov     QWORD PTR [rdi+8], 0       |       mov     QWORD PTR [rdi+8], 0
                                       |       cmp     rdi, rsi
                                       |       je      .L1
    mov     rax, QWORD PTR [rsi+8]     |       mov     rax, QWORD PTR [rsi+8]
    mov     QWORD PTR [rdi+8], rax     |       mov     QWORD PTR [rdi+8], rax
    mov     rax, QWORD PTR [rsi]       |       mov     rax, QWORD PTR [rsi]
    mov     QWORD PTR [rdi], rax       |       mov     QWORD PTR [rdi], rax
    mov     QWORD PTR [rsi+8], 0       |       mov     QWORD PTR [rsi+8], 0
    mov     QWORD PTR [rsi], 0         |       mov     QWORD PTR [rsi], 0
                                       |   .L1:
    ret                                |       rep ret

除了正确版本的附加分支外,代码完全相同。含义:重复的分配已被删除

为什么要增加分支?Microsoft 页面定义的移动赋值运算符比移动构造函数做更多的工作:它可以防止自赋值。移动构造函数不受此保护。但是:正如我已经说过的,构造函数在几乎所有情况下都会被内联。在这些情况下,优化器可以看到它不是自赋值,所以这个分支也会被优化掉。


这得到了很多重复,但很重要:不要过早进行微优化!

不要误会我的意思,我也讨厌由于懒惰或马虎的开发人员或管理决策而浪费大量资源的软件。而节能不仅仅是电池,也是一个我非常热衷的环保话题。但是,过早地进行微优化在这方面没有帮助!当然,将大数据的算法复杂性和缓存友好性放在脑后。但在进行任何特定优化之前,请先测量!

在这种特定情况下,我什至猜想您永远不必手动优化,因为编译器将始终能够围绕您的移动构造函数生成最佳代码。现在进行无用的微优化将花费您以后的开发时间,因为您需要在两个地方更改代码,或者当您需要调试一个奇怪的错误时,该错误只会因为您只在一个地方更改代码而发生。那是浪费的开发时间,本来可以花在做有用的优化上。

于 2018-12-08T19:04:38.097 回答
13

我不会这样做。移动成员存在的原因首先是性能。为您的移动构造器执行此操作就像为超级汽车掏出百万美元,然后尝试通过购买普通汽油来省钱。

如果您想减少编写的代码量,请不要编写移动成员。您的班级将在移动上下文中很好地复制。

如果您希望您的代码具有高性能,请尽可能快地定制您的移动构造函数和移动分配。好的移动成员会非常快,你应该通过计算负载、商店和分支来估计他们的速度。如果您可以使用 4 个加载/存储而不是 8 个来编写一些东西,那就去做吧!如果你可以写一些没有分支而不是 1 的东西,那就去做吧!

当您(或您的客户)将您的课程放入 a时std::vector,您的类型会生成很多动作。即使您的移动速度在 8 次加载/存储时快如闪电,如果您只需 4 或 6 次加载/存储就可以使其速度提高两倍,甚至快 50%,恕我直言,这是值得的时间。

就我个人而言,我厌倦了看到等待的光标,并且愿意多花 5 分钟来编写我的代码,并且知道它是尽可能快的。

如果您仍然不相信这是值得的,请以两种方式编写它,然后在完全优化的情况下检查生成的程序集。谁知道呢,你的编译器可能足够聪明,可以为你优化掉额外的负载和存储。但是此时您已经投入了比刚开始编写优化移动构造函数更多的时间。

于 2013-06-15T00:23:01.507 回答
4

我的 C++11 版本的MemoryBlock课程。

#include <algorithm>
#include <vector>
// #include <stdio.h>

class MemoryBlock
{
 public:
  explicit MemoryBlock(size_t length)
    : length_(length),
      data_(new int[length])
  {
    // printf("allocating %zd\n", length);
  }

  ~MemoryBlock() noexcept
  {
    delete[] data_;
  }

  // copy constructor
  MemoryBlock(const MemoryBlock& rhs)
    : MemoryBlock(rhs.length_) // delegating to another ctor
  {
    std::copy(rhs.data_, rhs.data_ + length_, data_);
  }

  // move constructor
  MemoryBlock(MemoryBlock&& rhs) noexcept
    : length_(rhs.length_),
      data_(rhs.data_)
  {
    rhs.length_ = 0;
    rhs.data_ = nullptr;
  }

  // unifying assignment operator.
  // move assignment is not needed.
  MemoryBlock& operator=(MemoryBlock rhs) // yes, pass-by-value
  {
    swap(rhs);
    return *this;
  }

  size_t Length() const
  {
    return length_;
  }

  void swap(MemoryBlock& rhs)
  {
    std::swap(length_, rhs.length_);
    std::swap(data_, rhs.data_);
  }

 private:
  size_t length_;  // note, the prefix underscore is reserved.
  int*   data_;
};

int main()
{
   std::vector<MemoryBlock> v;
   // v.reserve(10);
   v.push_back(MemoryBlock(25));
   v.push_back(MemoryBlock(75));

   v.insert(v.begin() + 1, MemoryBlock(50));
}

使用正确的 C++11 编译器,MemoryBlock::MemoryBlock(size_t)在测试程序中只应调用 3 次。

于 2013-10-10T21:43:04.793 回答
1

我认为您不会注意到显着的性能差异。我认为使用移动构造函数中的移动赋值运算符是一种很好的做法。

但是我宁愿使用 std::forward 而不是 std::move 因为它更合乎逻辑:

*this = std::forward<MemoryBlock>(other);
于 2013-06-14T22:42:44.127 回答
0

这取决于您的移动赋值运算符的作用。如果您查看链接到的文章中的那个,您会部分看到:

  // Free the existing resource.
  delete[] _data;

因此,在这种情况下,如果您在没有先初始化的情况下从移动构造函数调用移动赋值运算符_data,您最终会尝试删除未初始化的指针。所以在这个例子中,不管是否低效,初始化值实际上是至关重要的。

于 2013-06-15T07:25:09.313 回答
-1

我会简单地消除成员初始化并编写,

MemoryBlock(MemoryBlock&& other)
{
   *this = std::move(other);
}

除非移动分配抛出异常,否则这将始终有效,而且通常不会!

这种风格的优点:

  1. 您无需担心编译器是否会双重初始化成员,因为这在不同的环境中可能会有所不同。
  2. 你写的代码更少。
  3. 即使您将来向班级添加额外的成员,您也不需要更新它。
  4. 编译器通常可以内联移动赋值,因此复制构造函数的开销将是最小的。

我认为@Howard 的帖子并没有完全回答这个问题。在实践中,类通常不喜欢复制,很多类只是禁用了复制构造函数和复制赋值。但是大多数类即使不可复制也是可以移动的。

于 2016-09-22T14:31:03.427 回答