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

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

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

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

Выезд: stackoverflow.com/questions/106586/…

Martin York 21.11.2008 10:56

Также: stackoverflow.com/questions/1331220/…

Jonas Byström 06.06.2011 03:21
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
116
2
31 486
7
Перейти к ответу Данный вопрос помечен как решенный

Ответы 7

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

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

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

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

«Накладные расходы на исключения возникают из-за того, что компилятор должен генерировать код, чтобы отслеживать, какие объекты должны быть уничтожены в каждом кадре стека (или, точнее, в области видимости)». Разве компилятор не должен делать это в любом случае, чтобы уничтожить объекты из возврата?

user34537 21.11.2008 07:35

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

MSalters 21.11.2008 13:21

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

Dmitry 01.04.2018 06:15

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

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

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

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

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

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

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

Uhli 01.06.2011 11:08

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

Ответ принят как подходящий

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

Итак, резюме. Не стоит, если не выбрасываются исключения. Некоторые затраты при возникновении исключения, но вопрос в том, «Является ли эта стоимость более высокой, чем использование и тестирование кодов ошибок вплоть до кода обработки ошибок».

Martin York 21.11.2008 06:57

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

MSalters 21.11.2008 13:24

На некоторых процессорах, таких как ARM, возврат к адресу на восемь «лишних» байтов после инструкции «bl» [переход и ссылка, также известный как «вызов»] будет стоить столько же, сколько возврат к адресу, следующему сразу за «бл». Мне интересно, как эффективность простого наличия каждого «bl», за которым следует адрес обработчика «входящего исключения», будет сравниваться с эффективностью подхода на основе таблиц, и делают ли какие-либо компиляторы такие вещи. Самая большая опасность, которую я вижу, заключается в том, что несоответствующие соглашения о вызовах могут вызвать странное поведение.

supercat 30.01.2012 22:56

@supercat: таким образом вы загрязняете свой I-кеш кодом обработки исключений. В конце концов, есть причина, по которой код обработки исключений и таблицы, как правило, далеки от обычного кода.

CesarB 01.02.2012 16:38

@CesarB: одно командное слово после каждого вызова. Это не кажется слишком возмутительным, особенно с учетом того, что методы обработки исключений с использованием только «внешнего» кода обычно требуют, чтобы код всегда поддерживал действительный указатель кадра (что в некоторых случаях может потребовать 0 дополнительных инструкций, но в других может потребоваться больше, чем один).

supercat 01.02.2012 19:58

Это отличная информация. Однако я хотел бы указать, что eh_frame нужно где-то хранить. Для ПК с подкачкой по запросу это преимущество, если это не соответствует нормальному пути кода. Однако в некоторых системах нет диска или MMU, поэтому таблицы исключений должны храниться в ОЗУ. Они очень большие. В этом простом примере кода они примерно в том же порядке, что и сам код! Это не критика поста или C++! Однако мне кажется, что многие люди задаются вопросом, почему исключения не прижились во встроенных системах. Размеры таблиц могут быть уменьшены, поэтому в некоторых системах они не являются ограничивающими.

artless noise 12.08.2019 17:05

Одна вещь, которая мне все еще немного неясна, - это то, как на самом деле проверяется, было ли создано исключение. Внутри блока try потенциально может быть выброшен любой оператор (пока игнорируем noexcept). Это что, установлен флаг CPU, например, переполнение?

flowit 01.06.2020 12:11

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

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

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

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

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

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

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

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

speedplane 05.01.2016 08:14

@speedplane: Полагаю. Но весь смысл компиляторов в том, что нам не нужно разбираться в оборудовании (он обеспечивает уровень абстракции). С современными компиляторами я сомневаюсь, что вы сможете найти хоть одного человека, который понимает все аспекты современного компилятора C++. Итак, почему понимание исключений отличается от понимания сложной функции X.

Martin York 05.01.2016 09:08

Вам всегда нужно иметь представление о том, что делает оборудование, это вопрос степени. Многие из тех, кто использует C++ (поверх Java или языка сценариев), часто делают это для повышения производительности. Для них слой абстракции должен быть относительно прозрачным, чтобы вы имели некоторое представление о том, что происходит с металлом.

speedplane 05.01.2016 09:58

@speedplane: тогда они должны использовать C, где уровень абстракции намного тоньше по дизайну.

Martin York 05.01.2016 20:30

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