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

Может ли оптимизирующий компилятор добавить std:: move?

Может ли компилятор сделать автоматическое преобразование lvalue-to-rvalue, если он может доказать, что lvalue больше не будет использоваться? Вот пример, чтобы уточнить, что я имею в виду:

void Foo(vector<int> values) { ...}

void Bar() {
  vector<int> my_values {1, 2, 3};
  Foo(my_values);  // may the compiler pretend I used std::move here?
}

Если в прокомментированную строку добавлен std::move, тогда вектор можно переместить в параметр Foo, а не скопировать. Однако, как написано, я не использовал std::move.

Довольно легко статически доказать, что my_values ​​не будут использоваться после прокомментированной строки. Итак, компилятор разрешил перемещать вектор, или ему нужно его скопировать?

4b9b3361

Ответ 1

Компилятор должен вести себя как - если копия произошла от vector до вызова Foo.

Если компилятор может доказать, что существует допустимое поведение абстрактной машины без видимых побочных эффектов (в рамках поведения абстрактной машины, а не на реальном компьютере!), который включает перемещение std::vector в Foo, он может сделайте это.

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

Возможно, наблюдаемое поведение при копировании std::vector<T>:

  • Вызов конструкторов копирования для элементов. Делать это с помощью int нельзя.
  • Вызов по умолчанию std::allocator<> в разное время. Это вызывает ::new и ::delete (возможно, 1). В любом случае ::new и ::delete не были заменены в вышеуказанной программе, поэтому вы не можете наблюдать это под стандартом.
  • Вызов деструктора T больше раз на разные объекты. Не наблюдается с помощью int.
  • vector не является пустым после вызова Foo. Никто не рассматривает это, поэтому он пуст, как если бы он не был.
  • Ссылки или указатели или итераторы к элементам внешнего вектора отличаются от внешних. Никакие ссылки, векторы или указатели не переносятся в элементы вектора вне Foo.

Пока вы можете сказать "но что, если система потеряла память, а вектор большой, разве это не наблюдаемо?":

Абстрактная машина не имеет условия "из памяти", она просто имеет распределение иногда с ошибкой (бросание std::bad_alloc) по не ограниченным причинам. Это не приводит к действительным действиям абстрактной машины, и не может быть не вызвано не распределением (фактической) памяти (на фактическом компьютере), поскольку несуществование памяти не имеет наблюдаемых побочных эффектов.

Немного больше игрового случая:

int main() {
  int* x = new int[std::size_t(-1)];
  delete[] x;
}

в то время как эта программа явно выделяет слишком много памяти, компилятор может свободно ничего не выделять.

Мы можем идти дальше. Равномерно:

int main() {
  int* x = new int[std::size_t(-1)];
  x[std::size_t(-2)] = 2;
  std::cout << x[std::size_t(-2)] << '\n';
  delete[] x;
}

можно преобразовать в std::cout << 2 << '\n';. Этот большой буфер должен существовать абстрактно, но до тех пор, пока ваша "настоящая" программа ведет себя как-будто бы абстрактная машина, на самом деле не должна ее выделять.

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


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


Важным фактом является то, что существуют ситуации, когда компилятор не должен вести себя как-если бы была копия, даже если std::move не был вызван.

Когда вы return, локальная переменная из функции в строке, которая выглядит как return X; и X, является идентификатором, а эта локальная переменная имеет продолжительность автоматического хранения (в стеке), операция неявно перемещение, и компилятор (если он может) может исключить существование возвращаемого значения и локальной переменной в один объект (и даже опустить move).

То же самое верно, когда вы строите объект из временного - операция неявно перемещается (поскольку она привязана к rvalue) и может полностью отойти от перемещения.

В обоих случаях компилятор должен рассматривать это как перемещение (а не копию), и он может ускорить ход.

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return x;
}

что X не имеет std::move, но он перемещается в возвращаемое значение, и эта операция может быть отменена (X, а возвращаемое значение может быть превращено в один объект).

Это:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return std::move(x);
}

блокирует elision, как это делает:

std::vector<int> foo(std::vector<int> x) {
  return x;
}

и мы можем даже заблокировать ход:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return (std::vector<int> const&)x;
}

или даже:

std::vector<int> foo() {
  std::vector<int> x = {1,2,3,4};
  return 0,x;
}

поскольку правила неявного перемещения преднамеренно хрупки. (0,x - использование сильно заклятого оператора ,).

Теперь, полагаясь на неявное перемещение, не возникающее в таких случаях, как этот последний ,, не рекомендуется: стандартный комитет уже изменил случай с неявной копией на неявный-перемещение, поскольку неявный-перемещение было добавлено в потому что они считали это безобидным (где функция возвращает тип A с A(B&&) ctor, а оператор return return b;, где b имеет тип b; в выпуске С++ 11, который сделал копия, теперь она делает ход.) Нельзя исключить дальнейшее расширение неявного перемещения: явно использовать const&, вероятно, самый надежный способ предотвратить его сейчас и в будущем.

Ответ 2

В этом случае компилятор может выйти из my_values. Это происходит потому, что это не вызывает различий в наблюдаемом поведении.

Цитирование стандартного определения наблюдаемого поведения на С++:

Наименьшие требования к соответствующей реализации:

  • Доступ к неустойчивым объектам оценивается строго в соответствии с правилами абстрактной машины.
  • При завершении программы все данные, записанные в файлы, должны быть идентичны одному из возможных результатов выполнения программы в соответствии с абстрактной семантикой.
  • Динамика входных и выходных сигналов интерактивных устройств должна происходить таким образом, чтобы запрос вывода фактически доставлялся до того, как программа ждет ввода. Что представляет собой интерактивное устройство, оно определяется реализацией.

Интерпретация этого немного: "файлы" здесь включают стандартный выходной поток и вызовы функций, которые не определены стандартом С++ (например, вызовы операционной системы или вызовы в сторонние библиотеки), следует предположить, что эти функции могут записываться в файл, поэтому следствием этого является то, что нестандартные вызовы функций также должны рассматриваться как наблюдаемые.

Однако ваш код (как вы его показали) не имеет переменных volatile и не вызывает нестандартных функций. Таким образом, две версии (move or not-move) должны иметь одинаковое наблюдаемое поведение, и поэтому компилятор мог бы (либо даже полностью оптимизировать функцию, и т.д.).

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