Як працюють винятки (за кадром) в c ++


109

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

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

Як насправді працюють винятки?



Відповіді:


105

Замість того, щоб здогадуватися, я вирішив фактично подивитися на згенерований код із невеликим шматочком коду C ++ та дещо старою установкою Linux.

class MyException
{
public:
    MyException() { }
    ~MyException() { }
};

void my_throwing_function(bool throwit)
{
    if (throwit)
        throw MyException();
}

void another_function();
void log(unsigned count);

void my_catching_function()
{
    log(0);
    try
    {
        log(1);
        another_function();
        log(2);
    }
    catch (const MyException& e)
    {
        log(3);
    }
    log(4);
}

Я скомпілював це g++ -m32 -W -Wall -O3 -save-temps -cі переглянув згенерований файл складання.

    .file   "foo.cpp"
    .section    .text._ZN11MyExceptionD1Ev,"axG",@progbits,_ZN11MyExceptionD1Ev,comdat
    .align 2
    .p2align 4,,15
    .weak   _ZN11MyExceptionD1Ev
    .type   _ZN11MyExceptionD1Ev, @function
_ZN11MyExceptionD1Ev:
.LFB7:
    pushl   %ebp
.LCFI0:
    movl    %esp, %ebp
.LCFI1:
    popl    %ebp
    ret
.LFE7:
    .size   _ZN11MyExceptionD1Ev, .-_ZN11MyExceptionD1Ev

_ZN11MyExceptionD1Evє MyException::~MyException(), тому компілятор вирішив, що потрібна копія деструктора, яка не є вбудованою.

.globl __gxx_personality_v0
.globl _Unwind_Resume
    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_catching_functionv
    .type   _Z20my_catching_functionv, @function
_Z20my_catching_functionv:
.LFB9:
    pushl   %ebp
.LCFI2:
    movl    %esp, %ebp
.LCFI3:
    pushl   %ebx
.LCFI4:
    subl    $20, %esp
.LCFI5:
    movl    $0, (%esp)
.LEHB0:
    call    _Z3logj
.LEHE0:
    movl    $1, (%esp)
.LEHB1:
    call    _Z3logj
    call    _Z16another_functionv
    movl    $2, (%esp)
    call    _Z3logj
.LEHE1:
.L5:
    movl    $4, (%esp)
.LEHB2:
    call    _Z3logj
    addl    $20, %esp
    popl    %ebx
    popl    %ebp
    ret
.L12:
    subl    $1, %edx
    movl    %eax, %ebx
    je  .L16
.L14:
    movl    %ebx, (%esp)
    call    _Unwind_Resume
.LEHE2:
.L16:
.L6:
    movl    %eax, (%esp)
    call    __cxa_begin_catch
    movl    $3, (%esp)
.LEHB3:
    call    _Z3logj
.LEHE3:
    call    __cxa_end_catch
    .p2align 4,,3
    jmp .L5
.L11:
.L8:
    movl    %eax, %ebx
    .p2align 4,,6
    call    __cxa_end_catch
    .p2align 4,,6
    jmp .L14
.LFE9:
    .size   _Z20my_catching_functionv, .-_Z20my_catching_functionv
    .section    .gcc_except_table,"a",@progbits
    .align 4
.LLSDA9:
    .byte   0xff
    .byte   0x0
    .uleb128 .LLSDATT9-.LLSDATTD9
.LLSDATTD9:
    .byte   0x1
    .uleb128 .LLSDACSE9-.LLSDACSB9
.LLSDACSB9:
    .uleb128 .LEHB0-.LFB9
    .uleb128 .LEHE0-.LEHB0
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB1-.LFB9
    .uleb128 .LEHE1-.LEHB1
    .uleb128 .L12-.LFB9
    .uleb128 0x1
    .uleb128 .LEHB2-.LFB9
    .uleb128 .LEHE2-.LEHB2
    .uleb128 0x0
    .uleb128 0x0
    .uleb128 .LEHB3-.LFB9
    .uleb128 .LEHE3-.LEHB3
    .uleb128 .L11-.LFB9
    .uleb128 0x0
.LLSDACSE9:
    .byte   0x1
    .byte   0x0
    .align 4
    .long   _ZTI11MyException
.LLSDATT9:

Сюрприз! На нормальному шляху коду взагалі немає додаткових інструкцій. Натомість компілятор генерував додаткові позалінійні блоки фіксації кодів, на які посилається таблиця в кінці функції (яка фактично розміщується в окремому розділі виконуваного файлу). Вся робота виконується за лаштунками стандартною бібліотекою на основі цих таблиць ( _ZTI11MyExceptionє typeinfo for MyException).

Добре, це насправді не було сюрпризом для мене, я вже знав, як це робив цей компілятор. Продовжуючи вихід з монтажу:

    .text
    .align 2
    .p2align 4,,15
.globl _Z20my_throwing_functionb
    .type   _Z20my_throwing_functionb, @function
_Z20my_throwing_functionb:
.LFB8:
    pushl   %ebp
.LCFI6:
    movl    %esp, %ebp
.LCFI7:
    subl    $24, %esp
.LCFI8:
    cmpb    $0, 8(%ebp)
    jne .L21
    leave
    ret
.L21:
    movl    $1, (%esp)
    call    __cxa_allocate_exception
    movl    $_ZN11MyExceptionD1Ev, 8(%esp)
    movl    $_ZTI11MyException, 4(%esp)
    movl    %eax, (%esp)
    call    __cxa_throw
.LFE8:
    .size   _Z20my_throwing_functionb, .-_Z20my_throwing_functionb

Тут ми бачимо код для кидання винятку. Хоча зайвих накладних витрат не було просто тому, що може бути викинуто виняток, очевидно, що накладні витрати фактично кидають і ловлять виняток. Більша частина його прихована всередині __cxa_throw, яка повинна:

  • Пройдіться стеком за допомогою таблиць винятків, поки не знайде обробник цього винятку.
  • Розкручуйте стек, поки не потрапить до цього обробника.
  • Фактично зателефонуйте обробнику.

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

Для закінчення решта файлу складання:

    .weak   _ZTI11MyException
    .section    .rodata._ZTI11MyException,"aG",@progbits,_ZTI11MyException,comdat
    .align 4
    .type   _ZTI11MyException, @object
    .size   _ZTI11MyException, 8
_ZTI11MyException:
    .long   _ZTVN10__cxxabiv117__class_type_infoE+8
    .long   _ZTS11MyException
    .weak   _ZTS11MyException
    .section    .rodata._ZTS11MyException,"aG",@progbits,_ZTS11MyException,comdat
    .type   _ZTS11MyException, @object
    .size   _ZTS11MyException, 14
_ZTS11MyException:
    .string "11MyException"

Дані typeinfo.

    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zPL"
    .uleb128 0x1
    .sleb128 -4
    .byte   0x8
    .uleb128 0x6
    .byte   0x0
    .long   __gxx_personality_v0
    .byte   0x0
    .byte   0xc
    .uleb128 0x4
    .uleb128 0x4
    .byte   0x88
    .uleb128 0x1
    .align 4
.LECIE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB9
    .long   .LFE9-.LFB9
    .uleb128 0x4
    .long   .LLSDA9
    .byte   0x4
    .long   .LCFI2-.LFB9
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x5
    .byte   0x4
    .long   .LCFI5-.LCFI3
    .byte   0x83
    .uleb128 0x3
    .align 4
.LEFDE3:
.LSFDE5:
    .long   .LEFDE5-.LASFDE5
.LASFDE5:
    .long   .LASFDE5-.Lframe1
    .long   .LFB8
    .long   .LFE8-.LFB8
    .uleb128 0x4
    .long   0x0
    .byte   0x4
    .long   .LCFI6-.LFB8
    .byte   0xe
    .uleb128 0x8
    .byte   0x85
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI7-.LCFI6
    .byte   0xd
    .uleb128 0x5
    .align 4
.LEFDE5:
    .ident  "GCC: (GNU) 4.1.2 (Ubuntu 4.1.2-0ubuntu4)"
    .section    .note.GNU-stack,"",@progbits

Ще більше винятків, що обробляють таблиці та різноманітну інформацію.

Отже, висновок, принаймні для GCC в Linux: вартість - це додатковий простір (для обробників та таблиць), кидаються винятки чи ні, плюс додаткові витрати на розбір таблиць та виконання обробників при викиді винятку. Якщо ви використовуєте винятки замість кодів помилок, а помилка рідкісна, вона може бути швидшою , оскільки у вас більше немає витрат на тестування помилок.

Якщо ви хочете отримати більше інформації, зокрема про всі __cxa_функції, дивіться оригінальну специфікацію, з якої вони походять:


23
Тож підсумок. Не варто, якщо не виключаються винятки. Деякі кошти, коли викинутий виняток, але питання: "Чи більша ця вартість, ніж використання та тестування кодів помилок аж до коду обробки помилок".
Мартін Йорк

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

У деяких процесорах, таких як ARM, повернення до адреси восьми "зайвих" байтів повз інструкцію "bl" [гілка і зв'язок, також відома як "виклик"] буде коштувати стільки ж, як і повернення до адреси відразу після "бл". Мені цікаво, як ефективність просто мати кожний "bl" з наступною адресою обробника "вхідного винятку" порівнюватиметься з табличним підходом, і чи роблять якісь компілятори подібні дії. Найбільша небезпека, яку я можу побачити, полягає в тому, що невідповідні умови виклику можуть викликати нерозумну поведінку.
supercat

2
@supercat: таким чином ви забруднюєте свій I-кеш, використовуючи код обробки винятків. Існує причина, що обробка винятків з кодом і таблицями, як правило, далеко від звичайного коду.
CesarB

1
@CesarB: Одне інструкційне слово після кожного дзвінка. Це не здається занадто обурливим, особливо з огляду на те, що методи обробки винятків, що використовують лише "зовнішній" код, зазвичай вимагають, щоб код підтримував дійсний покажчик кадру весь час (що в деяких випадках може вимагати 0 додаткових інструкцій, а в інших може вимагати більше один).
supercat

13

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

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

Вартість коду обробки винятків, коли не використовуються винятки, практично дорівнює нулю.

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

Також є одна ґутка для новачків:
Хоча об'єкти винятку мають бути невеликими, деякі люди поміщають у них багато речей. Тоді у вас є вартість копіювання об'єкта виключення. Рішення там два рази:

  • Не ставте зайві речі за вашим винятком.
  • Ловити по const посилання.

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


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

@speedplane: Я думаю. Але вся суть компіляторів полягає в тому, що нам не потрібно розуміти апаратне забезпечення (воно забезпечує рівень абстракції). З сучасними компіляторами я сумніваюся, чи зможете ви знайти єдину людину, яка розуміє всі аспекти сучасного компілятора C ++. То чому розуміння винятків відрізняється від розуміння складної особливості X.
Мартін Йорк

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

@speedplane: Тоді вони повинні використовувати C, де шар абстракції за конструкцією значно тонший.
Мартін Йорк

12

Існує кілька способів реалізувати винятки, але, як правило, вони покладаються на деяку підтримку ОС. У Windows це структурований механізм обробки винятків.

Існує гідне обговорення деталей Code Project: Як компілятор C ++ реалізує обробку виключень

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

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


"Накладні винятки виникають через те, що компілятор повинен генерувати код, щоб відслідковувати, які об'єкти повинні бути знищені у кожному кадрі стека (або точніше області)" Чи не повинен компілятор все-таки робити це для руйнування об'єктів під час повернення?

Ні. Враховуючи стек із зворотними адресами та таблицею, компілятор може визначити, які функції є у ​​стеку. З того, які об’єкти, мабуть, були у стеці. Це можна зробити після викиду винятку. Трохи дорого, але потрібно лише тоді, коли виняток насправді кидається.
MSalters

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

6

Метт П'єтрек написав чудову статтю про обробку винятків Win32 Structured . Хоча ця стаття була спочатку написана в 1997 році, вона застосовується і сьогодні (але, звичайно, стосується лише Windows).


5

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



0

Усі хороші відповіді.

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

Мій девіз: легко написати код, який працює. Найголовніше - написати код для наступної людини, яка на нього дивиться. У деяких випадках це ти за 9 місяців, і ти не хочеш лаяти своє ім’я!


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