Как работают исключения (за кулисами) в C++
-
08-07-2019 - |
Вопрос
Я продолжаю видеть, как люди говорят, что исключения происходят медленно, но я никогда не вижу никаких доказательств.Поэтому вместо того, чтобы спрашивать, есть ли они, я спрошу, как исключения работают скрыто, чтобы я мог принять решение о том, когда их использовать и являются ли они медленными.
Насколько я знаю, исключения — это то же самое, что и возврат нескольких данных, но он также проверяет, когда нужно прекратить возврат.Как он проверяет, когда следует остановиться?Я предполагаю и говорю, что есть второй стек, который содержит тип исключения и местоположение стека, а затем возвращает результаты, пока не доберется туда.Я также предполагаю, что единственный раз, когда стек касается касания, - это бросок и каждая попытка/ловля.Реализация 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 ++ обрабатывает исключения несколько лет назад.
Все хорошие ответы.
Кроме того, подумайте о том, насколько проще отлаживать код, который выполняет проверку «если проверяет» как элементы в начале методов, а не позволяет коду генерировать исключения.
Мой девиз - легко написать код, который работает. Самое главное, чтобы написать код для следующего человека, который смотрит на него. В некоторых случаях это вы через 9 месяцев, и вы не хотите проклинать свое имя!