Я спостерігав за систематичною обробкою помилок у C ++ - Андрій Александреску, він стверджує, що винятки в C ++ дуже повільні.
Чи все ще це справедливо для С ++ 98?
Я спостерігав за систематичною обробкою помилок у C ++ - Андрій Александреску, він стверджує, що винятки в C ++ дуже повільні.
Чи все ще це справедливо для С ++ 98?
Відповіді:
Основною моделлю, яка сьогодні використовується для винятків (Itanium ABI, VC ++ 64 біти), є винятки з нульової вартості.
Ідея полягає в тому, що замість того, щоб втрачати час, встановлюючи охорону та явно перевіряючи наявність винятків скрізь, компілятор генерує допоміжну таблицю, яка відображає будь-яку точку, яка може викликати виняток (лічильник програм), до списку обробників. Коли створюється виняток, для пошуку правильного обробника (якщо такий є) проглядається цей список, і стек розмотується.
Порівняно із типовою if (error)стратегією:
ifколи трапляється винятокОднак вартість не є тривіальною для вимірювання:
dynamic_castтест для кожного обробника)Отже, переважно помилки кешу, і, отже, не тривіальні порівняно з чистим процесорним кодом.
Примітка: докладніше прочитайте звіт TR18015, розділ 5.4 Обробка винятків (pdf)
Так, так, винятки повільні на винятковому шляху , але в іншому випадку вони швидші, ніж явні перевірки ( ifстратегія) загалом.
Примітка: Андрій Александреску, здається, ставить під сумнів це "швидше". Я особисто бачив, як все хитається в обидві сторони, деякі програми швидше за винятками, а інші швидше з гілками, тому справді, здається, втрата оптимізації в певних умовах.
Це важливо ?
Я б стверджував, що ні. Програма повинна бути написана з урахуванням читабельності , а не продуктивності (принаймні, не як першого критерію). Винятки слід використовувати, коли передбачається, що абонент не може або не захоче усунути несправність на місці, і передасть його в стек. Бонус: у C ++ 11 винятки можна маршувати між потоками за допомогою стандартної бібліотеки.
Це тонко, хоча, я стверджую, що map::findне слід кидати, але мені добре map::findповернути а, checked_ptrяка кидає, якщо спроба розмежування не вдається, оскільки вона є нульовою: в останньому випадку, як і у випадку з класом, який ввів Александреску, абонент обирає між явною перевіркою та опорою на винятки. Надання повноважень абоненту, не надаючи йому більшої відповідальності, як правило, є ознакою гарного дизайну.
abort, дозволить вам виміряти розмір двійкового розміру та перевірити, чи відповідає поведінка часу завантаження / кеш-пам’яті подібним чином. Звичайно, краще не бити нічого з abort...
Коли питання було опубліковано, я їхав до лікаря, таксі чекало, тож я встиг тоді лише на короткий коментар. Але тепер, коли коментували, проголосували і проти, я краще додав би власну відповідь. Навіть якщо відповідь Матьє вже досить гарна.
Повторний позов
"Я спостерігав за систематичною обробкою помилок в 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
Проблеми з кешуванням (наприклад, коротший код може поміститися в кеші)
У звіті є інший перелік аспектів, які слід врахувати, але в будь-якому випадку єдиним практичним способом отримати вагомі факти щодо ефективності виконання є, мабуть, реалізація тієї самої програми з використанням винятків, а не з використанням винятків, у межах визначеного обмеження часу розробки та з розробниками. знайомі з кожним способом, а потім ВИМІРЮЙТЕ .
Правильність майже завжди перевершує ефективність.
Без винятків, легко може статися таке:
Деякий код P призначений для отримання ресурсу або обчислення певної інформації.
Викличний код С повинен був перевірити на успіх / помилку, але ні.
У коді після 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 ++ 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.
longjmpобробник.
try..finallyКонструкція може бути реалізована без стека розкручування. F #, C # і Java всі реалізуються try..finallyбез використання розгортання стека. Ви просто longjmpдо обробника (як я вже пояснював).
Це залежить від компілятора.
Наприклад, GCC, як відомо, мав дуже низьку ефективність при обробці винятків, але це стало значно кращим за останні кілька років.
Але зауважте, що обробка винятків, як зазначено в назві, повинна бути винятком, а не правилом у розробці програмного забезпечення. Коли у вас є програма, яка видає стільки винятків за секунду, що це впливає на продуктивність, і це все ще вважається нормальною роботою, тоді вам краще подумати про те, щоб робити щось інакше.
Винятки - це чудовий спосіб зробити код більш читабельним, усунувши з місця весь той незграбний код обробки помилок, але як тільки вони стають частиною звичайного потоку програм, їм стає дуже важко слідувати. Пам’ятайте, що a throw- це майже goto catchзамасковане.
throw new Exception- це Java-ізм. як правило, ніколи не слід кидати вказівники.
Ви ніколи не можете заявити про продуктивність, якщо не перетворите код у збірку або не встановите його.
Ось, що ви бачите: (швидкий тест)
Код помилки не чутливий до відсотка випадків. Винятки мають трохи накладних витрат, якщо їх ніколи не кидають. Як тільки ви їх кинете, біда починається. У цьому прикладі він використовується для 0%, 1%, 10%, 50% та 90% випадків. Коли винятки викидаються 90% випадків, код в 8 разів повільніший, ніж у випадку, коли винятки викидаються 10% часу. Як бачите, винятки дійсно повільні. Не використовуйте їх, якщо їх часто кидають. Якщо у вашому додатку немає вимог у режимі реального часу, сміливо кидайте їх, якщо вони трапляються дуже рідко.
Ви бачите багато суперечливих думок щодо них. Але нарешті, хіба винятки повільні? Я не суджу. Просто стежте за еталоном.
Так, але це не має значення. Чому?
Прочитайте це:
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 ...
Як і у Silico, сказано, що його реалізація залежить, але загалом винятки вважаються повільними для будь-якої реалізації та не повинні використовуватися в коді з інтенсивним виконанням.
РЕДАКТУВАТИ: Я не кажу, що взагалі їх не використовуйте, але для інтенсивного виконання найкраще уникати їх.