Я читал о семантике перемещения и считаю, что действительно эффективным свопом является следующее (сейчас мы будем хранить все как int вместо использования дженериков для простоты):
void swap(int& a, int& b) {
int tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
Это правильная реализация подкачки (для целых чисел), верно? Строка 2 не создает дополнительный элемент в оперативной памяти для tmp
, верно?
Другой мой вопрос: делает ли этот же код, использующий ссылки вместо std::move, такую же хорошую работу (например, не создавая дополнительную переменную для tmp
), как пример семантики перемещения выше?
void swap(int& a, int& b) {
int tmp = &a;
a = &b;
b = &tmp;
}
Создает ли приведенный выше код дополнительную переменную для tmp
? Правильно ли поменять местами a
и b
?
Ссылка — это указатель с добавленными дополнительными ограничениями (не может быть переназначен, не может создать их массив). Почему бы просто не передавать указатели и не использовать <T>* tmp = a; *a = b; *b = tmp;
? Если не просто поменять местами указатели, вам нужно создать полный дополнительный объект как tmp, чтобы удерживать
@Dave - «Ссылка — это указатель» - не с точки зрения стандарта, это не так. Хотя использование указателя - это то, сколько часто используемых компиляторов воплощать в жизнь ссылается под капотом.
int tmp = &a;
— ошибка (указатель нельзя присвоить целому без приведения)
Обмен с использованием ссылок будет выглядеть так:
void swap(int& a, int& b) {
int tmp = a;
a = b;
b = tmp;
}
Вы ставите &
только при объявлении ссылки, но не при ее использовании. Ссылки можно использовать как обычные переменные.
Поскольку вы работаете с целыми числами, этот обмен так же эффективен, как и обмен с std::move
. Семантика перемещения дает преимущество только тогда, когда вы работаете с классами, которые владеют ресурсами.
Например, вектору принадлежит указатель на массив, поэтому перемещение вектора намного эффективнее, чем копирование вектора, потому что при копировании вектора необходимо скопировать все данные во внутреннем массиве вектора. С другой стороны, если вы перемещаете вектор, ему не нужно копировать внутренний массив. Ему нужно только скопировать указатель на массив, и это намного, намного быстрее.
У встроенных типов нет конструктора перемещения или оператора присваивания перемещения, поэтому ваша первая версия ничем не отличается от предыдущей.
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
Ваша вторая версия не будет компилироваться. Однако, скорее всего, вы просто хотели написать то же самое, что и моя версия выше. Обе версии вводят временную переменную с именем tmp
для хранения значения, которое необходимо присвоить одному swappee, в то время как значение другого изменяется.
Не полагайтесь на то, что каждая конструкция в вашем коде, такая как переменная, имеет только одну соответствующую конструкцию в сгенерированном машинном коде, такую как «дополнительный элемент в ОЗУ», к которому она всегда будет приводить. Это не так. современные компиляторы работают. Работа компилятора заключается не в том, чтобы брать каждую строку исходного кода и выполнять замену 1:1 определенной последовательностью машинных инструкций. Задача компилятора состоит в том, чтобы взять всю программу, которую вы описали на языке C++, и сгенерировать программу эквивалент, например, в машинном коде, которая при выполнении будет вести себя неотличимо от поведения программы. который описан в вашем исходном коде C++.
Если вы сгенерируете взгляните на сборку, вы увидите, что в случае замены простых целых чисел ни один здравомыслящий компилятор не переместит эту временную переменную в память, а будет использовать регистры для замены значений. Вы можете доверять своему компилятору, который знает, какой самый эффективный способ поменять местами два целых числа в данной целевой архитектуре. Если вы не можете доверять своему компилятору, то вам следует искать лучший компилятор...
На самом деле, трудно сказать, насколько «эффективна» эта функция swap
, просто взглянув на код, который она генерирует изолированно. На практике такая функция, как swap
выше, обычно должна быть полностью оптимизирована. Единственный след, который он обычно оставляет в машинном коде, — это эффект, который он оказывает на поток данных (пример здесь), но никогда не будет фактической инструкции вызова для вызова отдельного фрагмента машинного кода для выполнения подкачки. Когда дело доходит до оптимизации В самом деле, важно убедиться, что компилятор могу действительно оптимизирует его (что обычно сводится к тому, чтобы убедиться, что его определение известно везде, где оно используется).
Мораль этой истории: сосредоточьтесь не на описании того, как, по вашему мнению, должен работать машинный код, а на выражении ваших намерений таким образом, который будет понятен как вам, так и компилятору, и, в первую очередь, таким образом, чтобы правильно описывал задуманное. поведение на основе правил языка, а не правил целевого оборудования. Никогда не полагайтесь на то, что вы думать отображает определенная языковая конструкция на машинном уровне. Полагайтесь на то, что язык C++ говорит о поведении вызывает определенная языковая конструкция. Современные компиляторы C++ отлично переводят сложный код C++ в очень компактный и эффективный машинный код, который делает точно то, что было выражено в C++. Однако они делают это, агрессивно используя тот факт, что выраженное поведение также является поведением единственный, которое необходимо уважать. В результате современные компиляторы C++ очень плохой генерируют машинный код, который не делает того, что было написано, а просто думал в момент написания...
Нет никакой разницы между перемещением и копированием для типов POD. Лучшим тестовым примером было бы использование подвижного типа без POD, например
std::string
. И да, строка 2 выделяет память дляtmp
, а во втором примереtmp
всегда является копиейa
(и, кстати, вы используете&
внутри функции неправильно).