Подтвердить что ты не робот

Внедрение Move Constructor путем вызова оператора назначения переноса

Статья MSDN, Как написать Write Constuctor, имеет следующую рекомендацию.

Если вы предоставляете как конструктор перемещения, так и оператор назначения перемещения для своего класса, вы можете исключить избыточный код, написав конструктор перемещения, чтобы вызвать оператор назначения перемещения. в следующем примере показана измененная версия конструктора перемещения, которая вызывает оператор присваивания перемещения:

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

Является ли этот код неэффективным, дважды инициализируя значения MemoryBlock, или компилятор сможет оптимизировать дополнительные инициализации? Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?

4b9b3361

Ответ 1

Я бы так не сделал. Причиной появления элементов перемещения в первую очередь является производительность. Выполнение этого для вашего конструктора движений - это как обстрел мегабайков для суперкара, а затем попытка сэкономить деньги, покупая регулярный газ.

Если вы хотите уменьшить количество написанного кода, просто не записывайте элементы перемещения. Ваш класс будет копировать только отлично в контексте перемещения.

Если вы хотите, чтобы ваш код имел высокую производительность, а затем создайте конструктор перемещения и переместите назначение как можно быстрее. Хорошие участники движения будут стремительно быстрыми, и вы должны оценивать их скорость, подсчитывая нагрузки, магазины и ветки. Если вы можете написать что-то с 4 загрузками/магазинами вместо 8, сделайте это! Если вы можете написать что-то без ветвей вместо 1, сделайте это!

Когда вы (или ваш клиент) помещаете свой класс в std::vector, много движений может генерироваться в вашем типе. Даже если ваш ход будет молниеносно в 8 загрузочных/хранилищах, если вы можете сделать его в два раза быстрее или даже на 50% быстрее только с 4 или 6 загрузками/магазинами, imho, потраченное на время.

Лично мне надоело видеть ожидающих курсоров, и я желаю пожертвовать дополнительные 5 минут на то, чтобы написать свой код и знать, что он как можно быстрее.

Если вы все еще не уверены, что это того стоит, напишите его в обоих направлениях, а затем просмотрите сгенерированную сборку при полной оптимизации. Кто знает, ваш компилятор просто может быть достаточно умным, чтобы оптимизировать дополнительные нагрузки и магазины для вас. Но к этому времени вы уже вложили больше времени, чем если бы вы только что создали оптимизированный конструктор перемещения.

Ответ 2

Моя версия С++ 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));
}

С правильным компилятором С++ 11 MemoryBlock::MemoryBlock(size_t) следует вызывать только 3 раза в тестовой программе.

Ответ 3

Я не думаю, что вы заметите значительную разницу в производительности. Я считаю хорошей практикой использовать оператор присваивания перемещения из конструктора перемещения.

Однако я предпочел бы использовать std:: forward вместо std:: move, потому что это более логично:

*this = std::forward<MemoryBlock>(other);

Ответ 4

[...] сможет ли компилятор оптимизировать лишние инициализации?

Почти во всех случаях: да.

Должен ли я всегда писать свои конструкторы перемещения, вызывая оператор присваивания перемещения?

Да, просто реализуйте это с помощью оператора назначения перемещения, за исключением случаев, когда вы измерили, что это приводит к неоптимальной производительности.


Сегодня оптимизатор делает невероятную работу по оптимизации кода. Ваш пример кода особенно легко оптимизировать. Прежде всего: конструктор перемещения будет встроен почти во всех случаях. Если вы реализуете это с помощью оператора присваивания перемещения, он также будет встроен.

И давайте посмотрим на некоторые сборки! Здесь показан точный код с веб-сайта 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, выполняет больше работы, чем конструктор перемещения: он защищен от самостоятельного назначения. Конструктор перемещения не защищен от этого. Но: как я уже сказал, конструктор будет встроен практически во всех случаях. И в этих случаях оптимизатор может видеть, что это не самостоятельное назначение, поэтому эта ветвь также будет оптимизирована.


Это часто повторяется, но важно: не делайте преждевременной микро -O -птимизации!

Не поймите меня неправильно, я также ненавижу программное обеспечение, которое тратит много ресурсов из-за ленивых или небрежных разработчиков или управленческих решений. Экономия энергии - это не только батареи, но и экологическая тема, которой я очень увлечен. Но преждевременное проведение микро -O -пистилизаций в этом отношении не помогает! Конечно, сохраняйте алгоритмическую сложность и удобство кэширования ваших больших данных в своей голове. Но прежде чем делать какую-либо конкретную оптимизацию, измерьте!

В этом конкретном случае я бы даже предположил, что вам никогда не придется оптимизировать вручную, потому что компилятор всегда сможет генерировать оптимальный код вокруг вашего конструктора перемещения. Выполнение бесполезной микро--O -птимизации сейчас обойдется вам позже, когда вам нужно изменить код в двух местах или когда вам нужно отладить странную ошибку, которая возникает только потому, что вы изменили код только в одном месте. И это потраченное впустую время разработки, которое можно потратить на полезную оптимизацию.

Ответ 5

Это зависит от вашего оператора присваивания. Если вы посмотрите на ту, в которой вы связаны, вы частично видите:

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

Итак, в этом контексте, если вы сначала вызвали оператор присваивания перемещения из конструктора move без инициализации _data, вы в конечном итоге попытаетесь удалить неинициализированный указатель. Таким образом, в этом примере, неэффективно или нет, на самом деле важно, чтобы вы инициализировали значения.

Ответ 6

Я просто исключил бы инициализацию члена и напишу,

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

Это всегда будет работать, если исключение переноса не приведет к исключениям, и обычно это не так!

Преимущества этих стилей:

  • Вам не нужно беспокоиться о том, будет ли компилятор дважды инициализировать члены, потому что это может различаться в разных средах.
  • Вы пишете меньше кода.
  • Вам не нужно обновлять его, даже если вы добавите дополнительных членов в класс в будущем.
  • Компиляторы часто могут встраивать назначение перемещения, поэтому накладные расходы на конструктор копирования будут минимальными.

Я думаю, что сообщение @Howard не совсем отвечает на этот вопрос. На практике классы часто не любят копирование, многие классы просто отключают конструктор копирования и назначение копии. Но большинство классов могут быть перемещаемыми, даже если они не могут быть скопированы.