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


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

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

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

7 98

7 ответов:

вместо того, чтобы гадать, я решил на самом деле посмотреть на сгенерированный код с небольшим куском кода 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    , %esp
.LCFI5:
    movl    , (%esp)
.LEHB0:
    call    _Z3logj
.LEHE0:
    movl    , (%esp)
.LEHB1:
    call    _Z3logj
    call    _Z16another_functionv
    movl    , (%esp)
    call    _Z3logj
.LEHE1:
.L5:
    movl    , (%esp)
.LEHB2:
    call    _Z3logj
    addl    , %esp
    popl    %ebx
    popl    %ebp
    ret
.L12:
    subl    , %edx
    movl    %eax, %ebx
    je  .L16
.L14:
    movl    %ebx, (%esp)
    call    _Unwind_Resume
.LEHE2:
.L16:
.L6:
    movl    %eax, (%esp)
    call    __cxa_begin_catch
    movl    , (%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    , %esp
.LCFI8:
    cmpb    , 8(%ebp)
    jne .L21
    leave
    ret
.L21:
    movl    , (%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_ функции делают, см. первоначальную спецификацию они пришли от:

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

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

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

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

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

  • не ставьте дополнительные вещи в вашем исключении.
  • поймать по ссылке const.

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

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

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

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

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

Мэтт Pietrek написал отличную статью о Win32 Структурированная Обработка Исключений. Хотя эта статья была первоначально написана в 1997 году, он по-прежнему применяется сегодня (но, конечно, относится только к Windows).

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

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

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

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

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

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