Чи є винятки в C ++ дійсно повільними


98

Я спостерігав за систематичною обробкою помилок у C ++ - Андрій Александреску, він стверджує, що винятки в C ++ дуже повільні.

Чи все ще це справедливо для С ++ 98?


42
Немає сенсу запитувати, чи "винятки C ++ 98" швидші / повільніші, ніж "винятки C ++ 03" або "винятки C ++ 11". Їх продуктивність залежить від того, як компілятор впроваджує їх у ваші програми, а стандарт C ++ нічого не говорить про те, як їх слід впроваджувати; єдина вимога полягає в тому, що їх поведінка повинна відповідати стандарту (правило "як би").
In silico

Пов’язане (але насправді не повторюване) питання: stackoverflow.com/questions/691168/…
Філіпп

2
так, це дуже повільно, але їх не слід кидати для звичайних операцій або використовувати як гілку
BЈовић


Щоб пояснити сказане Бьовичем, боятися використання винятків - це не те, що потрібно. Коли виникає виняток, ви стикаєтесь (потенційно) з трудомісткими операціями. Мені також цікаво, чому ви хочете знати саме про C ++ 89 ... остання версія - це C ++ 11, і час, який потрібен для запуску винятків, визначений реалізацією, отже, мій "потенційно" трудомісткий час .
thecoshman

Відповіді:


162

Основною моделлю, яка сьогодні використовується для винятків (Itanium ABI, VC ++ 64 біти), є винятки з нульової вартості.

Ідея полягає в тому, що замість того, щоб втрачати час, встановлюючи охорону та явно перевіряючи наявність винятків скрізь, компілятор генерує допоміжну таблицю, яка відображає будь-яку точку, яка може викликати виняток (лічильник програм), до списку обробників. Коли створюється виняток, для пошуку правильного обробника (якщо такий є) проглядається цей список, і стек розмотується.

Порівняно із типовою if (error)стратегією:

  • модель з нульовою вартістю, як випливає з назви, є безкоштовною, коли не виникає винятків
  • це коштує близько 10x / 20x, ifколи трапляється виняток

Однак вартість не є тривіальною для вимірювання:

  • Бічний столик, як правило, холодний , і тому його витяг з пам'яті займає багато часу
  • Визначення правильного обробника включає RTTI: багато дескрипторів RTTI для отримання, розкиданих по пам'яті та складних операцій для запуску (в основному dynamic_castтест для кожного обробника)

Отже, переважно помилки кешу, і, отже, не тривіальні порівняно з чистим процесорним кодом.

Примітка: докладніше прочитайте звіт TR18015, розділ 5.4 Обробка винятків (pdf)

Так, так, винятки повільні на винятковому шляху , але в іншому випадку вони швидші, ніж явні перевірки ( ifстратегія) загалом.

Примітка: Андрій Александреску, здається, ставить під сумнів це "швидше". Я особисто бачив, як все хитається в обидві сторони, деякі програми швидше за винятками, а інші швидше з гілками, тому справді, здається, втрата оптимізації в певних умовах.


Це важливо ?

Я б стверджував, що ні. Програма повинна бути написана з урахуванням читабельності , а не продуктивності (принаймні, не як першого критерію). Винятки слід використовувати, коли передбачається, що абонент не може або не захоче усунути несправність на місці, і передасть його в стек. Бонус: у C ++ 11 винятки можна маршувати між потоками за допомогою стандартної бібліотеки.

Це тонко, хоча, я стверджую, що map::findне слід кидати, але мені добре map::findповернути а, checked_ptrяка кидає, якщо спроба розмежування не вдається, оскільки вона є нульовою: в останньому випадку, як і у випадку з класом, який ввів Александреску, абонент обирає між явною перевіркою та опорою на винятки. Надання повноважень абоненту, не надаючи йому більшої відповідальності, як правило, є ознакою гарного дизайну.


3
+1 Я хотів би додати лише чотири речі: (0) про підтримку відновлення, додану в C ++ 11; (1) посилання на звіт комітету щодо ефективності c ++; (2) деякі зауваження щодо правильності (як перевершення навіть читабельності); та (3) щодо продуктивності, зауваження щодо її вимірювання у випадку невикористання винятків (все відносно)
Вітаємо та hth. - Альф

2
@ Cheersandhth.-Alf: (0), (1) та (3) виконано: спасибі. Що стосується правильності (2), хоча вона перевершує читабельність, я не впевнений у винятках, що призводять до більш правильного коду, ніж інші стратегії обробки помилок (так легко забути про безліч невидимих ​​шляхів виконання винятків).
Matthieu M.

2
Опис може бути локально коректним, але, можливо, варто зазначити, що наявність винятків має глобальні наслідки для припущень та оптимізацій, які може зробити компілятор. Ці наслідки страждають від того, що у них "немає тривіальних контра-прикладів", оскільки компілятор завжди може бачити невелику програму. Профілювання на реалістичній, великій основі коду з винятками та без них може бути гарною ідеєю.
Kerrek SB

4
> Модель з нульовою вартістю, як випливає з назви, є безкоштовною, коли не виникає винятків, це насправді не відповідає найтоншим рівням деталізації. генерація більше коду завжди впливає на продуктивність, навіть якщо вона невелика і незначна ... ОС може зайняти трохи більше часу, щоб ОС завантажила виконуваний файл, або ви отримаєте більше помилок i-cache. також, як щодо коду для розмотування стека? також, як щодо експериментів, які ви можете провести для вимірювання ефектів, замість того, щоб намагатися зрозуміти це за допомогою раціональної думки?
jheriko

2
@jheriko: Я вважаю, що насправді я вже звертався до більшості ваших запитань. На час завантаження не слід впливати (холодний код не слід завантажувати), на i-кеш не слід впливати (холодний код не повинен потрапляти в i-кеш), ... щоб вирішити одне відсутні питання: "як виміряти" => заміна будь-якого виключення, викликаного викликом abort, дозволить вам виміряти розмір двійкового розміру та перевірити, чи відповідає поведінка часу завантаження / кеш-пам’яті подібним чином. Звичайно, краще не бити нічого з abort...
Матьє М.

60

Коли питання було опубліковано, я їхав до лікаря, таксі чекало, тож я встиг тоді лише на короткий коментар. Але тепер, коли коментували, проголосували і проти, я краще додав би власну відповідь. Навіть якщо відповідь Матьє вже досить гарна.


Чи є винятки особливо повільними у C ++ порівняно з іншими мовами?

Повторний позов

"Я спостерігав за систематичною обробкою помилок в C ++ - Андрій Александреску, він стверджує, що винятки в C ++ дуже повільні".

Якщо це буквально стверджує Андрій, то раз він дуже вводить в оману, якщо не прямо помилятися. Для підвищених / кинутих винятків завжди повільно порівняно з іншими основними операціями в мові, незалежно від мови програмування . Не лише на С ++ або тим більше на С ++, ніж на інших мовах, як вказує передбачувана заява.

Загалом, в основному, незалежно від мови, дві основні мовні особливості, які на порядок повільніші за інші, оскільки вони перекладаються на виклики процедур, які обробляють складні структури даних, є

  • виняток метання, і

  • динамічне виділення пам'яті.

На щастя в C ++ часто можна уникнути обох у критично важливих для часу кодах.

На жаль, такої речі, як безкоштовний обід , не існує, навіть якщо ефективність C ++ за замовчуванням досить близька. :-) Для ефективності, отриманої шляхом уникнення викидів та динамічного розподілу пам'яті, як правило, досягається кодування на нижчому рівні абстракції, використовуючи С ++ як просто "кращий С". А нижча абстракція означає більшу “складність”.

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


Чи існують якісь об'єктивні показники продуктивності викидів C ++?

Так, міжнародний комітет стандартизації С ++ опублікував Технічний звіт про ефективність С ++, TR18015 .


Що означає, що винятки є «повільними»?

В основному це означає, що a throwможе зайняти дуже довгий час ™ порівняно з, наприклад, intпризначенням, завдяки пошуку обробника.

Як TR18015 обговорює у своєму розділі 5.4 “Винятки”, існує дві основні стратегії реалізації винятків,

  • підхід, коли кожен try-блок динамічно налаштовує зловживання винятків, так що пошук вгору по динамічному ланцюжку обробників виконується, коли виникає виняток, і

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

Перший дуже гнучкий та загальний підхід майже вимушений у 32-розрядної Windows, тоді як у 64-розрядної версії land та * nix-land зазвичай використовується другий набагато ефективніший підхід.

Окрім того, як обговорюється у цьому звіті, для кожного підходу існує три основні сфери, де обробка винятків впливає на ефективність:

  • try-блоки,

  • регулярні функції (можливості оптимізації), і

  • throw-вирази.

В основному, при підході динамічного обробника (32-розрядна Windows) обробка винятків впливає на tryблоки, здебільшого незалежно від мови (оскільки це вимушено схемою Windows Structured Handling Handling ), тоді як підхід до статичної таблиці має приблизно нульові витрати на try- блоків. Обговорення цього питання займе набагато більше місця та досліджень, ніж це є практичним для відповіді SO. Отже, детальніше див. У звіті.

На жаль, звіт за 2006 рік вже трохи датований станом на кінець 2012 року, і, наскільки мені відомо, немає нічого порівнянного, що є новішим.

Інша важлива перспектива полягає в тому, що вплив використання винятків на продуктивність сильно відрізняється від ізольованої ефективності допоміжних мовних функцій, оскільки, як зазначається у звіті,

"Розглядаючи обробку винятків, її слід протиставити альтернативним способам вирішення помилок".

Наприклад:

  • Витрати на обслуговування завдяки різним стилям програмування (правильність)

  • ifПеревірка виходу з ладу резервного виклику порівняно з централізованоюtry

  • Проблеми з кешуванням (наприклад, коротший код може поміститися в кеші)

У звіті є інший перелік аспектів, які слід врахувати, але в будь-якому випадку єдиним практичним способом отримати вагомі факти щодо ефективності виконання є, мабуть, реалізація тієї самої програми з використанням винятків, а не з використанням винятків, у межах визначеного обмеження часу розробки та з розробниками. знайомі з кожним способом, а потім ВИМІРЮЙТЕ .


Який хороший спосіб уникнути накладних винятків?

Правильність майже завжди перевершує ефективність.

Без винятків, легко може статися таке:

  1. Деякий код P призначений для отримання ресурсу або обчислення певної інформації.

  2. Викличний код С повинен був перевірити на успіх / помилку, але ні.

  3. У коді після C використовується неіснуючий ресурс або недійсна інформація, що спричиняє загальний хаос.

Основною проблемою є точка (2), де за звичайною схемою зворотного коду код виклику С не змушений перевіряти.

Існує два основних підходи, які змушують таку перевірку:

  • Де Р безпосередньо видає виняток, коли не вдається.

  • Де P повертає об'єкт, який C повинен перевірити перед використанням його основного значення (інакше виняток або припинення).

Другим підходом був AFAIK, вперше описаний Бартоном та Накменом у їхній книзі * Scientific and Engineering C ++: Introduction with Advanced Techniques and Examples , де вони представили клас, що вимагає Fallowотримання «можливого» результату функції. Зараз подібний клас називається optionalбібліотекою Boost. І ви можете легко реалізувати Optionalклас самостійно, використовуючи std::vectorяк носій значення для випадку не-POD результату.

З першим підходом викличний код C не має іншого вибору, як використовувати методи обробки винятків. Однак при другому підході викличний код С може сам вирішити, чи робити ifперевірку на основі, чи загальну обробку винятків. Таким чином, другий підхід підтримує компроміс між програмістом і компромісом щодо ефективності часу виконання.


Який вплив мають різні стандарти C ++ на продуктивність винятків?

"Я хочу знати, чи все ще це справедливо для C ++ 98"

C ++ 98 був першим стандартом C ++. Для винятків він запровадив стандартну ієрархію класів виключень (на жаль, досить недосконалу). Основним впливом на продуктивність була можливість специфікацій винятків (видалених у C ++ 11), які, однак, ніколи не були повністю реалізовані основним компілятором Windows C ++ Visual C ++: Visual C ++ приймає синтаксис специфікації винятків C ++ 98, але просто ігнорує специфікації винятків.

C ++ 03 був лише технічним виправленням C ++ 98. Єдиною справді новою в C ++ 03 була ініціалізація значень . Що не має нічого спільного з винятками.

Зі стандартними вимогами C ++ 11 загальні винятки були вилучені та замінені noexceptключовим словом.

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


6
"винятки завжди повільні порівняно з іншими основними операціями в мові, незалежно від мови програмування" ... за винятком мов, призначених для компіляції використання винятків у звичайний контроль потоку.
Ben Voigt

4
"створення винятку включає як розподіл, так і розкручування стека". Це також очевидно неправда загалом, і знову ж таки, OCaml є протилежним прикладом. У мовах, зібраних сміття, немає необхідності розкручувати стек, тому що немає деструкторів, тому вам потрібно лише longjmpобробник.
JD

2
@JonHarrop: мабуть, ти не знаєш, що Pyhon має пункт нарешті для обробки винятків. це означає, що реалізація Python або має розмотування стека, або не є Python. ви здаєтесь абсолютно не обізнаними в предметах, щодо яких ви заявляєте (фантазія). вибачте.
Вітаю і hth. - Альф

2
@ Cheersandhth.-Alf: "У Pyhon є пункт нарешті про обробку винятків. Це означає, що реалізація Python або розкручує стек, або не є Python". try..finallyКонструкція може бути реалізована без стека розкручування. F #, C # і Java всі реалізуються try..finallyбез використання розгортання стека. Ви просто longjmpдо обробника (як я вже пояснював).
JD

4
@JonHarrop: ви звучати як створює дилему. але це не має відношення, я бачу до будь-чого, що обговорювалось дотепер, і до цих пір ви опублікували довгу послідовність негативно звучащих дурниць . мені довелося б вам довіряти, щоб погодитись чи ні з якимись неясними формулюваннями, тому що як антагоніст ви вибираєте те, що ви виявите, що це "означає", і я, звичайно , не довіряю вам після всіх цих безглуздих дурниць, протистояння тощо.
Вітаю і hth. - Альф

12

Це залежить від компілятора.

Наприклад, GCC, як відомо, мав дуже низьку ефективність при обробці винятків, але це стало значно кращим за останні кілька років.

Але зауважте, що обробка винятків, як зазначено в назві, повинна бути винятком, а не правилом у розробці програмного забезпечення. Коли у вас є програма, яка видає стільки винятків за секунду, що це впливає на продуктивність, і це все ще вважається нормальною роботою, тоді вам краще подумати про те, щоб робити щось інакше.

Винятки - це чудовий спосіб зробити код більш читабельним, усунувши з місця весь той незграбний код обробки помилок, але як тільки вони стають частиною звичайного потоку програм, їм стає дуже важко слідувати. Пам’ятайте, що a throw- це майже goto catchзамасковане.


-1 щодо питання, як воно стоїть зараз, "чи це все ще справедливо для С ++ 98", що, безумовно, не залежить від компілятора. також ця відповідь throw new Exception- це Java-ізм. як правило, ніколи не слід кидати вказівники.
Вітаю і hth. - Альф

1
чи стандарт 98 визначає, як саме застосовувати винятки?
thecoshman

6
C ++ 98 є стандартом ISO, а не компілятором. Є багато компіляторів, які його реалізують.
Філіпп

3
@thecoshman: Ні. Стандарт C ++ нічого не говорить про те, як щось слід реалізовувати (за винятком частини стандарту "Обмеження реалізації").
In silico

2
@Insilico, тоді я можу лише зробити логічний висновок, що (шокуюче) саме реалізація визначається (читається, для компілятора), як виконуються винятки.
thecoshman

12

Ви ніколи не можете заявити про продуктивність, якщо не перетворите код у збірку або не встановите його.

Ось, що ви бачите: (швидкий тест)

Код помилки не чутливий до відсотка випадків. Винятки мають трохи накладних витрат, якщо їх ніколи не кидають. Як тільки ви їх кинете, біда починається. У цьому прикладі він використовується для 0%, 1%, 10%, 50% та 90% випадків. Коли винятки викидаються 90% випадків, код в 8 разів повільніший, ніж у випадку, коли винятки викидаються 10% часу. Як бачите, винятки дійсно повільні. Не використовуйте їх, якщо їх часто кидають. Якщо у вашому додатку немає вимог у режимі реального часу, сміливо кидайте їх, якщо вони трапляються дуже рідко.

Ви бачите багато суперечливих думок щодо них. Але нарешті, хіба винятки повільні? Я не суджу. Просто стежте за еталоном.

Тест продуктивності винятків для C ++


4

Так, але це не має значення. Чому?
Прочитайте це:
https://blogs.msdn.com/b/ericlippert/archive/2008/09/10/vexing-exceptions.aspx

В основному це говорить про те, що використання винятків, таких як описаний Александреску (уповільнення в 50 разів, оскільки вони використовують catchяк else), є просто неправильним. Це сказано для ppl, хто любить робити це так, якби я хотів би, щоб C ++ 22 :) додав щось на зразок:
(зверніть увагу, це повинно бути основною мовою, оскільки це в основному компілятор, що генерує код з існуючого)

result = attempt<lexical_cast<int>>("12345");  //lexical_cast is boost function, 'attempt'
//... is the language construct that pretty much generates function from lexical_cast, generated function is the same as the original one except that fact that throws are replaced by return(and exception type that was in place of the return is placed in a result, but NO exception is thrown)...     
//... By default std::exception is replaced, ofc precise configuration is possible
if (result)
{
     int x = result.get(); // or result.result;
}
else 
{
     // even possible to see what is the exception that would have happened in original function
     switch (result.exception_type())
     //...

}

PS також зауважте, що навіть якщо винятки є настільки повільними ... це не проблема, якщо ви не витрачаєте багато часу в цій частині коду під час виконання ... Наприклад, якщо поділ float повільний, і ви робите це в 4 рази швидше, це не має значення, якщо ви витрачаєте 0,3% свого часу на підрозділ FP ...


0

Як і у Silico, сказано, що його реалізація залежить, але загалом винятки вважаються повільними для будь-якої реалізації та не повинні використовуватися в коді з інтенсивним виконанням.

РЕДАКТУВАТИ: Я не кажу, що взагалі їх не використовуйте, але для інтенсивного виконання найкраще уникати їх.


9
Це в кращому випадку дуже спрощений спосіб розгляду виняткової продуктивності. Наприклад, GCC використовує реалізацію "з нульовою вартістю", коли ви не отримуєте показник продуктивності, якщо не створюються винятки. І винятки призначені для виняткових (тобто рідкісних) обставин, тому навіть якщо вони повільні за деякими показниками, це все ще недостатня причина, щоб не використовувати їх.
In silico

@insilico, якщо ти подивишся, чому я сказав, я не сказав, щоб не використовувати виключення крапку. Я вказав інтенсивний код продуктивності, це точна оцінка, я в основному працюю з gpgpus, і мене б постріляли, якщо я використовував винятки.
Chris McCabe
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.