Яка реальна накладна витрата на спробу / лову в C #?


94

Отже, я знаю, що try / catch додає трохи накладних витрат, і тому не є хорошим способом контролювати процес, але звідки беруться ці накладні витрати і який це фактичний вплив?

Відповіді:


52

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

Я не думаю, що це проблема, поки ви використовуєте винятки для ВИНЯТКОВОЇ поведінки (тому не ваш типовий, очікуваний шлях через програму).


33
Точніше: спробувати дешево, зловити дешево, кинути дорого. Якщо ви уникаєте спроб зловити, кидок все одно дорогий.
Програміст для Windows

4
Хммм - націнка не працює в коментарях. Повторити
HTTP 410,

2
@Windows programmer Статистика / джерело, будь ласка?
Капе

100

Тут слід зробити три пункти:

  • По-перше, у насправді наявність блоків try-catch у вашому коді майже не відповідає показнику продуктивності. Це не повинно враховуватись при спробі уникнути їхнього включення до вашої заяви. Хіт виконання виконується лише тоді, коли створюється виняток.

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

  • Я вважаю, що це одна з причин, чому загальна порада, якщо ви збираєтеся відновити виняток, полягає в тому, щоб просто throw;не замінювати виняток знову, або будувати новий, оскільки в цих випадках вся ця інформація стека переглядається, тоді як у простому кинь це все збереглося.


41
Також: Коли ви відновите виняток як "кинь колишній", ви втратите оригінальний трас стека і заміните його на поточний стек; рідко те, що хочеться. Якщо ви просто "кидаєте", то оригінальна траса стека у винятку зберігається.
Едді

@Eddie Orthrow new Exception("Wrapping layer’s error", ex);
binki

21

Ви запитуєте про накладні витрати на використання try / catch / нарешті, коли винятки не створюються, або накладні витрати на використання винятків для управління потоком процесу? Останнє дещо схоже на використання палички динаміту для запалення свічки до дня народження малюка, і пов’язані з цим накладні витрати потрапляють у такі області:

  • Ви можете очікувати додаткових помилок кеш-пам’яті через видалене виняток, яке отримує доступ до резидентних даних, які зазвичай не знаходяться в кеші.
  • Ви можете очікувати додаткових несправностей сторінок через викинуту виняток, що отримує доступ до коду та даних нерезидента, які зазвичай не входять у робочий набір вашої програми.

    • наприклад, кидання винятку вимагатиме, щоб CLR знаходив розташування блоків нарешті і ловити на основі поточного IP і зворотного IP кожного кадру, поки не буде оброблено виняток плюс блок фільтра.
    • додаткові витрати на будівництво та роздільну здатність імен для створення фреймів для діагностичних цілей, включаючи читання метаданих тощо.
    • обидва вищезазначені елементи, як правило, мають доступ до "холодного" коду та даних, тому можливі неполадки на сторінках, якщо у вас взагалі тиск пам'яті:

      • CLR намагається помістити код та дані, які використовуються рідко, далеко від даних, які часто використовуються для покращення місцевості, тому це працює проти вас, тому що ви змушуєте холод бути гарячим.
      • вартість несправностей твердої сторінки, якщо такі є, перевершить усе інше.
  • Типові ситуації лову часто бувають глибокими, тому вищезазначені ефекти, як правило, посилюються (збільшуючи ймовірність помилок сторінки).

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


6

З мого досвіду, найбільші накладні витрати полягають у тому, що насправді створюється виняток та обробляється його. Одного разу я працював над проектом, де код, подібний до наведеного, використовувався для перевірки, чи хтось мав право редагувати якийсь об’єкт. Цей метод HasRight () використовувався скрізь на рівні презентації, і його часто використовували для 100-ти об’єктів.

bool HasRight(string rightName, DomainObject obj) {
  try {
    CheckRight(rightName, obj);
    return true;
  }
  catch (Exception ex) {
    return false;
  }
}

void CheckRight(string rightName, DomainObject obj) {
  if (!_user.Rights.Contains(rightName))
    throw new Exception();
}

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

Тож я переробив його на наступне, що - згідно з пізнішими швидкими та брудними вимірами - приблизно на 2 порядки швидше:

bool HasRight(string rightName, DomainObject obj) {
  return _user.Rights.Contains(rightName);
}

void CheckRight(string rightName, DomainObject obj) {
  if (!HasRight(rightName, obj))
    throw new Exception();
}

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


1
Чому ви хочете кинути тут виняток? Ви можете розглянути справу про відсутність прав на місці.
ThunderGr

@ThunderGr це власне те, що я змінив, зробивши це на два порядки швидшим.
Тобі

5

Всупереч загальновизнаним теоріям, try/ catchможе мати значні наслідки для продуктивності, і саме це - виняток чи ні!

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

Перші за ці роки висвітлювались у кількох публікаціях блогів від MVP Microsoft, і я вірю, що ви могли їх легко знайти, але StackOverflow так піклується про вміст, тому я надаю посилання на деякі з них як доказ наповнення :

Існує також така відповідь, яка показує різницю між розібраним кодом з використанням та без використання try/ catch.

Здається настільки очевидним, що є накладні витрати, які очевидно спостерігаються при генерації коду, і ці накладні витрати, схоже, визнають люди, які цінують Microsoft! Але я повторюю Інтернет ...

Так, є десятки додаткових інструкцій MSIL для одного тривіального рядка коду, і це навіть не охоплює відключені оптимізації, тому технічно це мікрооптимізація.


Я опублікував відповідь кілька років тому, яку видалили, оскільки вона зосереджувалась на продуктивності програмістів (макрооптимізація).

Це прикро, оскільки жодна економія кількох наносекунд тут і там часу процесора, ймовірно, не компенсує багато накопичених годин ручної оптимізації людьми. За що ваш начальник платить більше: годину вашого часу чи годину з працюючим комп’ютером? У який момент ми витягуємо штепсель і визнаємо, що пора просто придбати швидший комп’ютер ?

Зрозуміло, нам слід оптимізувати наші пріоритети , а не лише наш код! У своїй останній відповіді я спирався на відмінності між двома фрагментами коду.

Використання try/ catch:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

Не використовується try/ catch:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Поміркуйте з точки зору розробника технічного обслуговування, який, швидше за все, витратить ваш час, якщо не на профілювання / оптимізацію (розглянуто вище), що, ймовірно, навіть не було б необхідним, якби не проблема try/ catch, то на прокрутку вихідний код ... Один із них має чотири додаткові рядки сміття!

Оскільки все більше і більше полів вводяться в клас, все це збірне сміття накопичується (як у вихідному, так і в розібраному коді) значно перевищуючи розумні рівні. Чотири зайві рядки на поле, і це завжди однакові рядки ... Хіба нас не навчили уникати повторення? Я припускаю, що ми могли б приховати try/ catchза якоюсь домашньою абстракцією, але ... тоді ми могли б просто уникати винятків (тобто використання Int.TryParse).

Це навіть не складний приклад; Я бачив спроби створення нових класів у try/ catch. Врахуйте, що весь код всередині конструктора може бути дискваліфікований з-за певних оптимізацій, які в іншому випадку автоматично застосовуються компілятором. Який кращий спосіб породити теорію про те, що компілятор повільний , на відміну від компілятора, виконує саме те, що йому наказали робити ?

Припускаючи, що згаданий конструктор видає виняток, і в результаті запускається якась помилка, поганий розробник технічного обслуговування потім повинен його відстежити. Це може бути не таким простим завданням, оскільки, на відміну від коду спагеті goto nightmare, try/ catchможе спричинити безлад у трьох вимірах , оскільки може рухатися вгору по стеку не просто в інші частини того самого методу, а й в інші класи та методи , все це буде спостерігати розробник технічного обслуговування, важкий шлях ! І все ж нам кажуть, що "гото небезпечно", ге!

Наприкінці я згадую, try/ catchмає свою перевагу, яка полягає в тому, що вона призначена для відключення оптимізації ! Це, якщо хочете, допомога у налагодженні ! Для цього він був розроблений, і саме для цього його слід використовувати ...

Я думаю, це теж позитивний момент. Він може бути використаний для відключення оптимізацій, які в іншому випадку можуть скалічити безпечні, здорові алгоритми передачі повідомлень для багатопотокових додатків, а також для виявлення можливих перегонових умов;) Це приблизно єдиний сценарій, який я можу придумати, щоб використовувати функцію try / catch. Навіть у цього є альтернативи.


Що оптимізації робити try, catchі finallyвідключити?

AKA

Як try, catchі finallyкорисний в якості засобу налагодження?

вони перешкоди для запису. Це походить від стандарту:

12.3.3.13 Заяви про спроби лову

Для виписки stmt форми:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
  • Стан визначеного призначення v на початку блоку спроб такий самий, як і визначений стан призначення v на початку stmt .
  • Визначений стан призначення v на початку catch-block-i (для будь-якого i ) такий самий, як і визначений стан призначення v на початку stmt .
  • Визначений стан призначення v у кінцевій точці stmt , безумовно, призначається, якщо (і лише тоді), коли v визначено в кінцевій точці try-block та кожного catch-block-i (для кожного i від 1 до n ).

Іншими словами, на початку кожного tryтвердження:

  • всі призначення, зроблені видимим об'єктам до введення tryоператора, повинні бути завершеними, що вимагає блокування потоку для початку, що робить це корисним для налагодження гоночних умов!
  • компілятору заборонено:
    • усунути невикористані присвоєння змінних, яким було визначено до tryоператора
    • реорганізувати або об'єднати будь-яке з його внутрішніх завдань (тобто див. моє перше посилання, якщо ви цього ще не зробили).
    • підняти призначення над цим бар'єром, відкласти присвоєння змінній, яка, як відомо, не буде використана до пізніше (якщо взагалі буде), або попередньо перенести пізніші призначення вперед, щоб зробити можливими інші оптимізації ...

Подібна історія справедлива для кожного catchтвердження; припустимо, у межах вашого tryвисловлювання (або конструктора, або функції, яку він викликає тощо), яку ви призначаєте цій безглуздій змінній (скажімо, garbage=42;), компілятор не може усунути цей вислів, як би це не мало значення для спостережуваної поведінки програми . Призначення має бути виконане до введення catchблоку.

Що того варте, finallyрозповідає подібну принизливу історію:

12.3.3.14 Спроби остаточних тверджень

Для випробування твердження stmt форми:

try try-block
finally finally-block

• Стан визначеного призначення v на початку блоку спроб такий самий, як і визначений стан призначення v на початку stmt .
• Стан визначеного призначення v на початку блоку нарешті такий самий, як і визначений стан призначення v на початку stmt .
• Визначений стан призначення v у кінцевій точці stmt , безумовно, присвоюється, якщо (і лише якщо): o v визначено в кінцевій точці блоку try o vбезумовно призначається в кінцевій точці нарешті-блоку. Якщо зроблено передачу потоку керування (наприклад, оператор goto ), яка починається всередині блоку try і закінчується поза блоком try , тоді v також вважається безумовно призначеною для цього керування передачею потоку, якщо v кінцевій точці нарешті блоку визначено v . (Це не єдиний випадок, якщо - якщо v визначено з іншої причини на цій передачі потоку управління, то це все ще вважається визначеним.)

12.3.3.15 Заяви "спробуй зловити"

Аналіз Певною призначення для спроби - зловити - нарешті твердження форми:

try try-block
catch ( ... ) catch-block-1
... 
catch ( ... ) catch-block-n
finally finally-block

виконується так, ніби оператор спроба - нарешті оператор, що включає оператор try - catch :

try { 
    try   
    try-block
    catch ( ... ) catch-block-1
    ...   
    catch ( ... ) catch-block-n
} 
finally finally-block

3

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

Отже, щоб завершити все написане тут:
1) Використовуйте блоки try..catch, щоб ловити несподівані помилки - майже відсутність покарання за продуктивність.
2) Не використовуйте винятки для виключених помилок, якщо ви можете цього уникнути.


3

Я писав статтю про це ще деякий час тому, що тоді багато людей запитувало про це. Ви можете знайти його та код тесту на веб- сайті http://www.blackwasp.co.uk/SpeedTestTryCatch.aspx .

Результатом є те, що для блоку try / catch існує невелика кількість накладних витрат, але настільки мала, що його слід ігнорувати. Однак якщо ви запускаєте блоки спроби / лову у циклах, які виконуються мільйони разів, можливо, ви захочете розглянути можливість переміщення блоку за межі циклу, якщо це можливо.

Ключова проблема продуктивності блоків try / catch - це коли ви насправді ловите виняток. Це може додати помітну затримку вашої програми. Звичайно, коли справа йде не так, більшість розробників (і багато користувачів) визнають паузу як виняток, який ось-ось має відбутися! Головне тут - не використовувати обробку винятків для звичайних операцій. Як випливає з назви, вони виняткові, і ви повинні зробити все можливе, щоб уникнути їх кидання. Не слід використовувати їх як частину очікуваного потоку програми, яка працює належним чином.


2

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


Мені не вдалося зв’язатися з вашим блогом (зв’язок закінчився; ви використовуєте try/ catchзанадто багато? Хе-хе), але ви, схоже, сперечаєтесь із мовною специфікацією та деякими MVP MS, які також писали блоги на цю тему, забезпечуючи вимірювання на противагу вашій пораді ... Я відкритий для припущення, що проведене мною дослідження є неправильним, але мені потрібно прочитати запис у вашому блозі, щоб побачити, що в ньому сказано.
аутист

На додаток до публікації в блозі @ Hafthor, ось ще одна публікація в блозі з кодом, спеціально написаним для перевірки різниці в швидкодії. Згідно з результатами, якщо у вас є виняток, що трапляється навіть лише 5% часу, код обробки винятків працює в 100 разів повільніше, ніж код обробки винятків. Стаття конкретно орієнтована на методи try-catchблок-блок tryparse(), але концепція однакова.

2

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

Для мене цей програміст та накладні витрати на якість є основним аргументом проти використання методу спроби-лову для процесу.

Накладні витрати комп’ютера на винятки незначні в порівнянні і, як правило, незначні з точки зору здатності програми відповідати реальним вимогам до продуктивності.


@Ritchard T, чому? У порівнянні з програмістом і накладні витрати на якість вони незначні.
ThunderGr

1

Мені дуже подобається допис у блозі Хафтора , і, щоб додати свої два центи до цього обговорення, я хотів би сказати, що мені завжди було легко, коли РАЙ ДАНИХ кидав лише один тип винятків (DataAccessException). Таким чином мій БІЗНЕС ШАР знає, на який виняток очікувати, і вловлює його. Потім, залежно від подальших бізнес-правил (тобто, якщо мій бізнес-об’єкт бере участь у робочому процесі тощо), я можу створити нове виняток (BusinessObjectException) або продовжити, не повторюючи / кидаючи.

Я б сказав, не соромтеся використовувати try..catch, коли це необхідно, і використовуйте його з розумом!

Наприклад, цей метод бере участь у робочому процесі ...

Коментарі?

public bool DeleteGallery(int id)
{
    try
    {
        using (var transaction = new DbTransactionManager())
        {
            try
            {
                transaction.BeginTransaction();

                _galleryRepository.DeleteGallery(id, transaction);
                _galleryRepository.DeletePictures(id, transaction);

                FileManager.DeleteAll(id);

                transaction.Commit();
            }
            catch (DataAccessException ex)
            {
                Logger.Log(ex);
                transaction.Rollback();                        
                throw new BusinessObjectException("Cannot delete gallery. Ensure business rules and try again.", ex);
            }
        }
    }
    catch (DbTransactionException ex)
    {
        Logger.Log(ex);
        throw new BusinessObjectException("Cannot delete gallery.", ex);
    }
    return true;
}

2
Девіде, ти міг би обернути дзвінок до "DeleteGallery" блоком try / catch?
Роб

Оскільки DeleteGallery є логічною функцією, мені здається, що додавання винятку туди не є корисним. Для цього потрібно, щоб виклик DeleteGallery був укладений у блок try / catch. If (! DeleteGallery (theid)) {// handle} мені здається більш значущим. у цьому конкретному прикладі.
ThunderGr

1

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

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

Вибачте за мою англійську, сподіваюся, що це вам допоможе. Ця інформація базується на цитованій книзі, для отримання додаткової інформації див. Розділ 8.5 Обробка винятків.


Під час виконання компілятор втратив картину. Для блоків try / catch мають бути накладні витрати, щоб CLR міг обробляти винятки. C # працює на .NET CLR (віртуальна машина). Мені здається, що накладні витрати на сам блок є мінімальними, коли немає винятків, але вартість CLR, що обробляє виняток, дуже значна.
ThunderGr

-4

Давайте проаналізуємо одну з найбільших можливих витрат блоку try / catch, коли він використовується там, де його не потрібно використовувати:

int x;
try {
    x = int.Parse("1234");
}
catch {
    return;
}
// some more code here...

І ось той без спроби / лову:

int x;
if (int.TryParse("1234", out x) == false) {
    return;
}
// some more code here

Не рахуючи незначного пробілу, можна помітити, що ці два еквівалентні фрагменти коду мають майже однакову довжину в байтах. Останній містить на 4 байти менше відступу. Це погано?

Щоб додати образи до шкоди, студент вирішує цикл, тоді як введення може бути проаналізовано як int. Рішення без try / catch може бути приблизно таким:

while (int.TryParse(...))
{
    ...
}

Але як це виглядає при використанні try / catch?

try {
    for (;;)
    {
        x = int.Parse(...);
        ...
    }
}
catch
{
    ...
}

Блоки try / catch - це магічні способи марно витрачати відступи, і ми все ще навіть не знаємо, чому це не вдалося! Уявіть, як почувається людина, яка робить налагодження, коли код продовжує виконуватись після серйозного логічного недоліку, а не зупиняється з приємною очевидною помилкою винятку. Блоки try / catch - це перевірка / санітарія даних ледачого.

Однією з менших витрат є те, що блоки try / catch дійсно відключають певні оптимізації: http://msmvps.com/blogs/peterritchie/archive/2007/06/22/performance-implications-of-try-catch-finally.aspx . Я думаю, це теж позитивний момент. Він може бути використаний для відключення оптимізацій, які в іншому випадку можуть скалічити безпечні, здорові алгоритми передачі повідомлень для багатопотокових додатків, а також для виявлення можливих перегонових умов;) Це приблизно єдиний сценарій, який я можу придумати, щоб використовувати функцію try / catch. Навіть у цього є альтернативи.


1
Я майже впевнений, що TryParse робить спробу {int x = int.Parse ("xxx"); return true;} catch {return false; } внутрішньо. Відступ не стосується питання, лише продуктивність та накладні витрати.
ThunderGr

@ThunderGr Або прочитайте нову відповідь, яку я опублікував. Він містить більше посилань, одним із яких є аналіз значного підвищення продуктивності, коли ви уникаєте Int.Parseна користь Int.TryParse.
аутист
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.