Чи може сучасний C ++ отримати продуктивність безкоштовно?


205

Іноді стверджується, що C ++ 11/14 може підвищити продуктивність навіть при простому компілюванні коду C ++ 98. Виправдання, як правило, узгоджується з семантикою переміщення, оскільки в деяких випадках конструктори rvalue автоматично генеруються або тепер є частиною STL. Тепер мені цікаво, чи раніше ці випадки фактично вже розглядалися RVO чи подібними оптимізаторами компілятора.

Моє питання тоді, чи можете ви надати фактичний приклад фрагмента коду C ++ 98, який без змін працює швидше, використовуючи компілятор, що підтримує нові мовні функції. Я розумію, що стандартний відповідний компілятор не вимагає робити копіювання елісією, і саме з цієї причини семантика переміщення може спричинити швидкість, але я хотів би побачити менш патологічний випадок, якщо хочете.

EDIT: Просто для того, щоб було зрозуміло, я не запитую, чи нові компілятори швидші, ніж старі компілятори, а скоріше, чи є код, за допомогою якого до моїх прапорців компілятора додається -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

1
@vsoftco, на таку ситуацію, на яку я натякав, але не міг придумати приклад: з того, що я пам’ятаю, якщо мені потрібно визначити конструктор копій, конструктор переміщення не буде генерований автоматично, що залишає нас дуже прості класи, де RVO, я думаю, завжди працює. Винятком може бути щось в поєднанні з контейнерами STL, де конструктори rvalue генеруються реалізатором бібліотеки (це означає, що я не повинен би нічого міняти в коді, щоб використовувати рухи).
тривога

класи не повинні бути простими, щоб не було конструктора копій. C ++ процвітає у значенні семантики, а винятком повинні бути конструктор копій, оператор присвоєння, деструктор тощо.
sp2danny

1
@Eric Дякую за посилання, було цікаво. Однак, швидко переглянувши це, переваги швидкості в ньому, здається, здебільшого пов'язані з додаванням std::moveта переміщенням конструкторів (які потребувалимуть змін до існуючого коду). Єдине, що насправді стосується мого запитання, - це речення "Ви отримуєте негайні переваги швидкості просто шляхом перекомпіляції", яке не підкріплено жодними прикладами (він згадує STL на тому ж слайді, як я робив у своєму запитанні, але нічого конкретного ). Я просив кілька прикладів. Якщо я читаю слайди неправильно, повідомте мене.
тривога

Відповіді:


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

Коли NRVO (оптимізація з назвою повернення) відмовляється, у C ++ 03 вона відновлюється при копіюванні, на C ++ 11 вона повертається назад у русі. Збої NRVO прості:

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 закінчується символом a 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перевантаження moves, тимчасове, у вектор v. Один std::stringбуфер виділяється і переміщується у вектор. Порожнє std::stringвідкидається.

Призначення

Викрадено з відповіді @ Jarod42 нижче.

Elision не може відбуватися із призначенням, але може перейти з банки.

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
@alarge так. Але для того, щоб конструктор переміщення був у багато разів ефективнішим, ніж конструктор копій, зазвичай доводиться переміщувати ресурси, а не копіювати їх. Без написання власних конструкторів переміщення (та просто перекомпіляції програми C ++ 03), stdконтейнери бібліотеки будуть оновлюватися moveконструкторами "безкоштовно", і (якщо ви не блокували її) конструкціями, які використовують вказані об'єкти ( і зазначені об'єкти) почнуть отримувати вільну конструкцію руху в ряді ситуацій. Багато з цих ситуацій охоплені елізією в C ++ 03: не всі.
Якк - Адам Невраумон

5
Це погана реалізація оптимізатора, оскільки, тому що об'єкти, що повертаються по-різному, повертаються, не мають часу накладання, RVO теоретично все ще можливий.
Бен Войгт

2
@alarge Є місця, де елісія виходить з ладу, як, наприклад, коли два об'єкти, що перекриваються, можуть перетягнутися на третину, але не один на одного. Потім потрібно перемістити в C ++ 11 і скопіювати в C ++ 03 (ігноруючи як-якщо). На практиці Елісіон часто крихкий. Використання stdконтейнерів, наведених вище, здебільшого пояснюється тим, що вони є дешевим для переміщення екзогенним способом копіювання типу, який ви отримуєте "безкоштовно" в C ++ 11 при перекомпіляції C ++ 03 vector::resizeЄ винятком: він використовує moveв C ++ 11.
Якк - Адам Невраумон

27
Я бачу лише одну загальну категорію, яка є семантикою руху, і 5 особливих випадків цього.
Йоханнес Шауб - ліб

3
@sebro Я розумію, ви не вважаєте, що "програми не виділяють багато 1000 тисяч кілобайт-розподілів, і замість цього переміщують покажчики навколо", щоб бути достатнім. Ви хочете приурочити результати. Мікро-орієнтири - це не більше доказів підвищення продуктивності, ніж доказ, який ви принципово робите менше. Не вистачає кількох 100 застосувань у реальному світі в широкому спектрі галузей, які профільовані завданнями реального світу з профілюванням, насправді не підтверджує. Я взяв розпливчасті претензії щодо "безкоштовного виконання" і виклав їх конкретні факти про відмінності в поведінці програми під C ++ 03 та C ++ 11.
Якк - Адам Невраумон

46

якщо у вас є щось на кшталт:

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

// some code

v = foo();

Ви отримали копію на C ++ 03, тоді як ви отримали завдання переміщення в C ++ 11. тож у вас є безкоштовна оптимізація в такому випадку.


4
@Yakk: Як відбувається копіювання елізії при призначенні?
Jarod42

2
@ Jarod42 Я також вважаю, що копіювання elision неможливо в призначенні, оскільки ліва частина вже побудована, і компілятор не має розумного способу знати, що робити зі "старими" даними після крадіжки ресурсів справа сторона руки. Але, можливо, я помиляюся, я хотів би дізнатися відповідь раз і назавжди. Копіювати elision має сенс, коли ви копіюєте конструкцію, оскільки об’єкт "свіжий" і немає проблеми вирішити, що робити зі старими даними. Наскільки я знаю, єдиним винятком є ​​таке: "Призначення можуть бути відхилені лише за правилом як-якщо"
vsoftco

4
Хороший код C ++ 03 вже змінився в цьому випадку, черезfoo().swap(v);
Бен Войгт

@BenVoigt впевнений, але не весь код оптимізований, і не всі місця, де це відбувається, легко дістатися.
Якк - Адам Невраумон

Елізія копії може працювати у призначенні, як, наприклад, @BenVoigt. Кращим терміном є RVO (оптимізація зворотного значення) і працює лише в тому випадку, якщо foo () був реалізований таким чином.
DrumM
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.