Как работают исключения (за кулисами) в c ++

109

Я все время вижу, как люди говорят, что исключения - это медленно, но я никогда не вижу никаких доказательств. Поэтому вместо того, чтобы спрашивать, есть ли они, я спрошу, как исключения работают за кулисами, чтобы я мог принимать решения о том, когда их использовать и медленные ли они.

Насколько я знаю, исключения - это то же самое, что и возврат кучу раз, за ​​исключением того, что он также проверяет после каждого возврата, нужно ли ему сделать еще один или остановиться. Как он проверяет, когда перестать возвращаться? Я предполагаю, что есть второй стек, который содержит тип исключения и местоположение стека, затем он возвращается, пока не попадет туда. Я также предполагаю, что этот второй стек затрагивается только при броске и при каждой попытке / улове. AFAICT, реализующий подобное поведение с кодами возврата, займет такое же время. Но это всего лишь предположение, поэтому я хочу знать, что происходит на самом деле.

Как на самом деле работают исключения?

программист
источник
1
Проверьте: stackoverflow.com/questions/106586/…
Мартин Йорк,
Также: stackoverflow.com/questions/1331220/…
Jonas Byström 05

Ответы:

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_функции, см. Исходную спецификацию, из которой они взяты:

CesarB
источник
23
Итак, резюме. Не стоит, если не выбрасываются исключения. Некоторые затраты при возникновении исключения, но вопрос в том, «Является ли эта стоимость больше, чем использование и тестирование кодов ошибок вплоть до кода обработки ошибок».
Мартин Йорк
5
Стоимость ошибки действительно, вероятно, больше. Код исключения, вполне возможно, все еще находится на диске! Поскольку код обработки ошибок удален из обычного кода, поведение кеша в случаях, не связанных с ошибками, улучшается.
MSalters
На некоторых процессорах, таких как ARM, возврат к адресу на восемь «лишних» байтов после инструкции «bl» [переход и ссылка, также известный как «вызов»] будет стоить столько же, сколько возврат к адресу, следующему сразу за «бл». Интересно, как эффективность простого наличия каждого «bl», за которым следует адрес обработчика «входящего исключения», будет сравниваться с эффективностью подхода на основе таблиц, и делают ли какие-либо компиляторы такие вещи. Самая большая опасность, которую я вижу, заключается в том, что несоответствие соглашений о вызовах может вызвать странное поведение.
supercat
2
@supercat: таким образом вы загрязняете свой I-кеш кодом обработки исключений. В конце концов, есть причина, по которой код обработки исключений и таблицы, как правило, далеки от обычного кода.
CesarB 01
1
@CesarB: одно командное слово после каждого вызова. Это не кажется слишком возмутительным, особенно с учетом того, что методы обработки исключений с использованием только «внешнего» кода обычно требуют, чтобы код всегда поддерживал действительный указатель кадра (что в некоторых случаях может потребовать 0 дополнительных инструкций, но в других может потребоваться больше, чем один).
supercat 01
13

В прежние времена медлительность в виде исключений была правдой.
В большинстве современных компиляторов это уже не так.

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

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

Когда генерируется исключение, уже выполняется некоторая работа.
Но вы должны сравнить это со стоимостью возврата кодов ошибок и их полной проверки до точки, где ошибка может быть обработана. И то, и другое требует больше времени на написание и поддержку.

Также есть одна хитрость для новичков:
хотя объекты Exception должны быть небольшими, некоторые люди помещают в них много чего. Тогда у вас есть затраты на копирование объекта исключения. Есть два решения:

  • Не добавляйте лишнего в свое исключение.
  • Поймать по константной ссылке.

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

Мартин Йорк
источник
1
Я готов поспорить, что люди не решаются использовать исключения не из-за кажущейся медлительности, а потому, что они не знают, как они реализованы и что они делают с вашим кодом. Тот факт, что они кажутся волшебными, раздражает многих людей, близких к металлу.
Speedplane 05
@speedplane: Полагаю. Но весь смысл компиляторов в том, что нам не нужно разбираться в оборудовании (он обеспечивает уровень абстракции). С современными компиляторами я сомневаюсь, что вы сможете найти хоть одного человека, который понимает все аспекты современного компилятора C ++. Так почему понимание исключений отличается от понимания сложной функции X.
Мартин Йорк
Вам всегда нужно иметь представление о том, что делает оборудование, это вопрос степени. Многие из тех, кто использует C ++ (поверх Java или языка сценариев), часто делают это для повышения производительности. Для них слой абстракции должен быть относительно прозрачным, чтобы вы имели некоторое представление о том, что происходит с металлом.
Speedplane 05
@speedplane: тогда они должны использовать C, где уровень абстракции намного тоньше по дизайну.
Мартин Йорк
12

Есть несколько способов реализовать исключения, но обычно они будут полагаться на некоторую базовую поддержку со стороны ОС. В Windows это структурированный механизм обработки исключений.

Подробно обсуждается Code Project: как компилятор C ++ реализует обработку исключений.

Накладные расходы на исключения возникают из-за того, что компилятор должен сгенерировать код, чтобы отслеживать, какие объекты должны быть уничтожены в каждом кадре стека (или, точнее, в области видимости), если исключение распространяется за пределы этой области. Если у функции нет локальных переменных в стеке, требующих вызова деструкторов, то она не должна иметь потери производительности в отношении обработки исключений.

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

Роб Уокер
источник
«Накладные расходы на исключения возникают из-за того, что компилятор должен генерировать код, чтобы отслеживать, какие объекты должны быть уничтожены в каждом кадре стека (или, точнее, в области видимости)». Разве компилятор не должен делать это в любом случае, чтобы уничтожить объекты из возврата?
Нет. Имея стек с адресами возврата и таблицу, компилятор может определить, какие функции находятся в стеке. Из того, какие объекты должны были быть в стеке. Это можно сделать после создания исключения. Немного дорого, но требуется только тогда, когда действительно создается исключение.
MSalters
весело, я просто подумал про себя: «Было бы здорово, если бы каждый кадр стека отслеживал количество объектов в нем, их типы, имена, чтобы моя функция могла копать стек и видеть, какие области видимости он унаследовал во время отладки» , и в некотором смысле это делает что-то подобное, но без ручного объявления таблицы в качестве первой переменной каждой области.
Дмитрий
5

В этой статье исследуется проблема и в основном делается вывод о том, что на практике исключения связаны с затратами времени выполнения, хотя стоимость довольно низка, если исключение не генерируется. Хорошая статья, рекомендую.

Аластер
источник
2

Один мой друг написал несколько лет назад, как Visual C ++ обрабатывает исключения.

http://www.xyzw.de/c160.html

Нильс Пипенбринк
источник
0

Все хорошие ответы.

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

Мой девиз - легко написать работающий код. Самое главное - написать код для следующего человека, который его посмотрит. В некоторых случаях это вы через 9 месяцев, и вы не хотите ругать свое имя!

Kieveli
источник
Я согласен в целом, но в некоторых случаях исключения могут упростить код. Подумайте об обработке ошибок в конструкторах ... - другими способами были бы а) возврат кодов ошибок по ссылочным параметрам или б) установка глобальных переменных
Ули 01