Как работают исключения (негласно) в C ++ - PullRequest
105 голосов
/ 21 ноября 2008

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

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

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

Ответы [ 7 ]

104 голосов
/ 21 ноября 2008

Вместо того, чтобы гадать, я решил посмотреть на сгенерированный код с небольшим фрагментом кода 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"

Данные типа info.

    .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_, см. Оригинальную спецификацию, из которой они получены:

13 голосов
/ 21 ноября 2008

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

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

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

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

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

  • Не кладите лишних вещей в ваше исключение.
  • Catch по константной ссылке.

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

12 голосов
/ 21 ноября 2008

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

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

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

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

6 голосов
/ 21 ноября 2008

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

5 голосов
/ 21 ноября 2008

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

2 голосов
/ 21 ноября 2008

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

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

1 голос
/ 21 ноября 2008

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

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

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

...