Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

Вместо угадывания я решил посмотреть на сгенерированный код с небольшим фрагментом кода на С++ и несколько старой установкой 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 is 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_, см. исходную спецификацию, из которой они пришли:

Ответ 2

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

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

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

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

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

  • Не добавляйте лишние вещи в свое исключение.
  • Catch by const reference.

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

Ответ 3

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

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

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

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

Ответ 4

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

Ответ 6

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

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

Ответ 7

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

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

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