Может ли современный C ++ получить производительность бесплатно?

205

Иногда утверждают, что C ++ 11/14 может повысить производительность даже при простой компиляции кода C ++ 98. Обоснование обычно происходит в соответствии с семантикой перемещения, поскольку в некоторых случаях конструкторы rvalue генерируются автоматически или теперь являются частью STL. Теперь мне интересно, были ли эти случаи ранее уже обрабатывались RVO или подобными оптимизациями компилятора.

Тогда у меня возникает вопрос: не могли бы вы привести пример кода на C ++ 98, который без изменений работает быстрее с использованием компилятора, поддерживающего новые возможности языка. Я понимаю, что стандартный компилятор не требует выполнения копирования, и именно по этой причине семантика перемещения может привести к скорости, но я хотел бы увидеть менее патологический случай, если хотите.

РЕДАКТИРОВАТЬ: Просто чтобы прояснить, я не спрашиваю, быстрее ли новые компиляторы, чем старые компиляторы, а скорее, если есть код, добавляющий -std = c ++ 14 к моим флагам компилятора, он будет работать быстрее (избегайте копий, но если вы может придумать что-нибудь еще кроме семантики перемещения, мне тоже будет интересно)

большой
источник
3
Помните, что оптимизация копирования и возврата значения выполняется при создании нового объекта с использованием конструктора копирования. Однако в операторе присваивания копии нет права на копирование (как это может быть, поскольку компилятор не знает, что делать с уже созданным объектом, который не является временным). Следовательно, в этом случае C ++ 11/14 выигрывает, давая вам возможность использовать оператор присваивания перемещения. Что касается вашего вопроса, я не думаю, что код C ++ 98 должен быть быстрее, если он компилируется компилятором C ++ 11/14, возможно, он быстрее, потому что компилятор новее.
vsoftco
27
Кроме того, код, использующий стандартную библиотеку, потенциально быстрее, даже если вы делаете его полностью совместимым с C ++ 98, потому что в C ++ 11/14 базовая библиотека использует внутреннюю семантику перемещения, когда это возможно. Таким образом, код, который выглядит одинаково в C ++ 98 и C ++ 11/14, будет (возможно) быстрее в последнем случае, когда вы используете стандартные объекты библиотеки, такие как векторы, списки и т. Д., И семантика перемещения имеет значение.
vsoftco 22.12.14
1
@vsoftco, это та ситуация, на которую я ссылался, но не смог придумать пример: из того, что я помню, если мне нужно будет определить конструктор копирования, конструктор перемещения не будет сгенерирован автоматически, что оставляет нас с очень простые классы, где RVO, я думаю, всегда работает. Исключением может быть что-то в сочетании с контейнерами STL, где конструкторы rvalue генерируются разработчиком библиотеки (то есть мне не нужно ничего менять в коде, чтобы он использовал ходы).
alarge
классы не должны быть простыми, чтобы не иметь конструктора копирования. C ++ основывается на семантике значений, и конструктор копирования, оператор присваивания, деструктор и т. Д. Должны быть исключением.
sp2danny
1
@Eric Спасибо за ссылку, было интересно. Однако, после быстрого просмотра, преимущества в скорости, по-видимому, достигаются в основном за счет добавления std::moveи перемещения конструкторов (что потребует внесения изменений в существующий код). Единственное, что действительно касалось моего вопроса, было предложение «Вы получаете немедленные преимущества в скорости просто путем перекомпиляции», которое не подкреплено какими-либо примерами (там упоминается STL на том же слайде, как я это сделал в своем вопросе, но ничего конкретного ). Я просил несколько примеров. Если я неправильно читаю слайды, дайте мне знать.
alarge

Ответы:

221

Мне известны 5 общих категорий, в которых перекомпиляция компилятора C ++ 03 в C ++ 11 может привести к неограниченному увеличению производительности, которое практически не связано с качеством реализации. Это все вариации семантики перемещения.

std::vector перераспределять

struct bar{
  std::vector<int> data;
};
std::vector<bar> foo(1);
foo.back().data.push_back(3);
foo.reserve(10); // two allocations and a delete occur in C++03

каждый раз , когда fooбуфер «s перераспределяется в C ++ 03 он копируется каждый vectorв bar.

В C ++ 11 вместо этого перемещается bar::datas, что в основном бесплатно.

В этом случае это зависит от оптимизации внутри stdконтейнера vector. В каждом случае ниже использование stdконтейнеров только потому, что они являются объектами C ++, которые имеют эффективную moveсемантику в C ++ 11 «автоматически» при обновлении компилятора. Объекты, которые не блокируют его и содержат stdконтейнер, также наследуют автоматически улучшенные moveконструкторы.

НРВО провал

Когда NRVO (оптимизация именованного возвращаемого значения) завершается неудачно, в C ++ 03 он возвращается к копии, а в C ++ 11 - к перемещению. Неудачи НРВО легки:

std::vector<int> foo(int count){
  std::vector<int> v; // oops
  if (count<=0) return std::vector<int>();
  v.reserve(count);
  for(int i=0;i<count;++i)
    v.push_back(i);
  return v;
}

или даже:

std::vector<int> foo(bool which) {
  std::vector<int> a, b;
  // do work, filling a and b, using the other for calculations
  if (which)
    return a;
  else
    return b;
}

У нас есть три значения - возвращаемое значение и два разных значения внутри функции. Elision позволяет объединять значения в функции с возвращаемым значением, но не друг с другом. Они оба не могут быть объединены с возвращаемым значением без объединения друг с другом.

Основная проблема заключается в том, что элиминация NRVO является хрупкой, и код с изменениями, не находящимися рядом с returnсайтом, может внезапно привести к значительному снижению производительности в этом месте без использования диагностической информации. В большинстве случаев сбоя NRVO C ++ 11 заканчивается moveкопией, а C ++ 03 заканчивается копией.

Возврат аргумента функции

Исключение также невозможно здесь:

std::set<int> func(std::set<int> in){
  return in;
}

в C ++ 11 это дешево: в C ++ 03 нет способа избежать копирования. Аргументы функций не могут быть исключены с возвращаемым значением, потому что время жизни и местоположение параметра и возвращаемого значения управляются вызывающим кодом.

Тем не менее, C ++ 11 может переходить от одного к другому. (В менее игрушечном примере что-то может быть сделано с set).

push_back или insert

Наконец, исключение в контейнеры не происходит: но C ++ 11 перегружает rvalue, перемещает операторы вставки, что сохраняет копии.

struct whatever {
  std::string data;
  int count;
  whatever( std::string d, int c ):data(d), count(c) {}
};
std::vector<whatever> v;
v.push_back( whatever("some long string goes here", 3) );

в C ++ 03 создается временный объект whatever, затем он копируется в вектор v. std::stringВыделено 2 буфера, каждый с одинаковыми данными, а один отбрасывается.

В C ++ 11 временный whateverсоздается. whatever&& push_backПерегрузки , то moveс , что временная в вектор v. Один std::stringбуфер выделяется и перемещается в вектор. Пустое std::stringотбрасывается.

присваивание

Украденный из ответа @ Jarod42 ниже.

Исключение не может произойти с назначением, но может произойти.

std::set<int> some_function();

std::set<int> some_value;

// code

some_value = some_function();

здесь some_functionвозвращает кандидата для исключения, но поскольку он не используется для непосредственного создания объекта, он не может быть исключен. В C ++ 03 вышеприведенное приводит к тому, что содержимое временного объекта копируется в some_value. В C ++ 11 он перемещен some_value, что в основном бесплатно.


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

MSVC 2013 реализует конструкторы перемещения в stdконтейнерах, но не синтезирует конструкторы перемещения для ваших типов.

Таким образом, типы, содержащие std::vectors и подобные, не получат таких улучшений в MSVC2013, но начнут получать их в MSVC2015.

clang и gcc уже давно реализовали неявные конструкторы перемещения. Компилятор Intel 2013 года будет поддерживать неявную генерацию конструкторов перемещения, если вы пропустите -Qoption,cpp,--gen_move_operations(по умолчанию они этого не делают, пытаясь обеспечить кросс-совместимость с MSVC2013).

Якк - Адам Невраумонт
источник
1
@ большой да. Но для того, чтобы конструктор перемещения был во много раз эффективнее конструктора копирования, он обычно должен перемещать ресурсы, а не копировать их. Без написания ваших собственных конструкторов перемещения (и просто перекомпиляции программы на C ++ 03) все stdконтейнеры библиотеки будут обновляться moveконструкторами «бесплатно» и (если вы не блокировали это) конструкции, использующие указанные объекты ( и упомянутые объекты) начнут получать свободное перемещение в ряде ситуаций. Многие из этих ситуаций рассматриваются в C ++ 03: не все.
Якк - Адам Невраумонт
5
Это плохая реализация оптимизатора, так как возвращаемые объекты с разными именами не имеют перекрывающегося времени жизни, RVO теоретически все еще возможен.
Бен Фойгт
2
@alarge Существуют места, где исключение не удается, например, когда два объекта с перекрывающимися временами жизни могут быть исключены в третий, но не друг в друга. Затем требуется перемещение в C ++ 11 и копирование в C ++ 03 (игнорируя как-будто). Elision часто хрупка на практике. Использование stdконтейнеров, приведенных выше, в основном объясняется тем, что они являются дешевыми в использовании, так как копируют тип, который вы получаете «бесплатно» в C ++ 11 при перекомпиляции C ++ 03. vector::resizeЯвляется исключением: он использует moveв C ++ 11.
Якк - Адам Невраумонт
27
Я вижу только 1 общую категорию, которая является семантикой перемещения, и 5 особых случаев.
Йоханнес Шауб - лит
3
@sebro Я понимаю, вы не считаете, что «программы не выделяют много тысяч килобайт, а вместо этого перемещают указатели вокруг», чтобы быть достаточными. Вы хотите рассчитать результаты. Микробенчмарки являются не более доказательством улучшения производительности, чем доказательством того, что вы в основном делаете меньше. Если не считать нескольких сотен реальных приложений в самых разных отраслях, профилирование с реальными задачами профилирование не является действительно доказательством. Я взял смутные заявления о «бесплатной производительности» и изложил им конкретные факты о различиях в поведении программ в C ++ 03 и C ++ 11.
Якк - Адам Невраумонт
46

если у вас есть что-то вроде:

std::vector<int> foo(); // function declaration.
std::vector<int> v;

// some code

v = foo();

Вы получили копию в C ++ 03, тогда как вы получили задание на перемещение в C ++ 11. так что у вас есть бесплатная оптимизация в этом случае.

Jarod42
источник
4
@Yakk: Как происходит копирование в задании?
Jarod42
2
@ Jarod42 Я также считаю, что удаление копии в назначении невозможно, поскольку левая часть уже создана, и у компилятора нет разумного способа узнать, что делать со «старыми» данными после кражи ресурсов справа. сторона Но, возможно, я ошибаюсь, я бы с удовольствием раз и навсегда узнал ответ. Исключение копирования имеет смысл, когда вы копируете конструкцию, поскольку объект является «свежим», и нет проблем с решением, что делать со старыми данными. Насколько я знаю, единственное исключение заключается в следующем: «Назначения могут быть отменены только на основе правила« как будто »»
vsoftco
4
Хороший код C ++ 03 уже сделал ход в этом случае, черезfoo().swap(v);
Бен Фойгт
@BenVoigt конечно, но не весь код оптимизирован, и не все места, где это происходит, легко достижимы.
Якк - Адам Невраумонт
Копия ellision может работать в задании, как говорит @BenVoigt. Лучшим термином является RVO (оптимизация возвращаемого значения), и он работает только в том случае, если foo () реализован таким образом.
DrumM