Вопрос

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

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

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

Это было полезно?

Решение

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top