本文介绍了在c ++中异常如何工作(在幕后)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我不停地看到人们说,例外很慢,但我从来没有看到任何证据。因此,不是问他们是否会问我们如何在例外情况下工作,所以我可以决定什么时候使用它们,如果他们很慢。



我知道的异常是一样的事情做一堆返回,但它也检查什么时候需要停止做返回。如何检查什么时候停止?我猜猜,说有第二个堆栈,它保存的异常和堆栈位置的类型,然后会返回,直到它到达那里。我也猜测,唯一的时间栈是触摸是一个投掷和每次尝试/捕获。 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 ,并查看生成的装配文件。

 。文件foo.cpp
.section .text._ZN11MyExceptionD1Ev,axG,@ progbits,_ZN11MyExceptionD1Ev,comdat
.align 2
.p2align 4,and 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

ret
.L21:
movl $ 1,(%esp)
调用__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 中,它必须:





  • 取消堆栈,直到找到该处理程序。

  • 实际上调用



与简单返回值的代价进行比较,您会看到为什么异常仅应用于异常返回。 / p>

完成汇编文件的其余部分:

  .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:
.string11MyException

typeinfo数据。

  .section .eh_frame,a,@ progbits 
.Lframe1:
.long .LECIE1-.LSCIE1
.LSCIE1:
.long 0x0
.byte 0x1
.stringzPL
.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
。字节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:
.identGCC:(GNU)4.1.2(Ubuntu 4.1.2-0ubuntu4)
.section .note.GNU-stack,,@ progbits

更多的异常处理表和额外的信息。



,结论,至少对于Linux上的GCC:成本是额外的空间(对于处理程序和表),无论是否抛出异常,以及抛出异常时解析表和执行处理程序的额外成本。如果您使用例外而不是错误代码,并且错误很少,可以更快地执行,因为您没有再次测试错误的开销。



如果你想要更多的信息,特别是所有的 __ cxa _ 函数,请看他们来自的原始规范:






I keep seeing people say that exceptions are slow but I never see any proof. So instead of asking if they are I will ask how do exceptions work behind the scene so I can make a decisions of when to use them and if they are slow.

From what I know exceptions are the same thing as doing a bunch of return but it also checks when it needs to stop doing the return. How does it check when to do stop? I am taking a guess and saying there is a second stack which holds the type of exception and stack location then does returns until it gets there. I am also guessing the only time that stack is touch is on a throw and every try/catch. AFAICT implementing a similar behaviour with return code would take the same amount of time. But this is all a guess so I want to know.

How do exceptions really work?

解决方案

Instead of guessing, I decided to actually look at the generated code with a small piece of C++ code and a somewhat old Linux install.

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);
}

I compiled it with g++ -m32 -W -Wall -O3 -save-temps -c, and looked at the generated assembly file.

    .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 is MyException::~MyException(), so the compiler decided it needed a non-inline copy of the destructor.

.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:

Surprise! There are no extra instructions at all on the normal code path. The compiler instead generated extra out-of-line fixup code blocks, referenced via a table at the end of the function (which is actually put on a separate section of the executable). All the work is done behind the scenes by the standard library, based on these tables (_ZTI11MyException is typeinfo for MyException).

OK, that was not actually a surprise for me, I already knew how this compiler did it. Continuing with the assembly output:

    .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

Here we see the code for throwing an exception. While there was no extra overhead simply because an exception might be thrown, there is obviously a lot of overhead in actually throwing and catching an exception. Most of it is hidden within __cxa_throw, which must:

  • Walk the stack with the help of the exception tables until it finds a handler for that exception.
  • Unwind the stack until it gets to that handler.
  • Actually call the handler.

Compare that with the cost of simply returning a value, and you see why exceptions should be used only for exceptional returns.

To finish, the rest of the assembly file:

    .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"

The typeinfo data.

    .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

Even more exception handling tables, and assorted extra information.

So, the conclusion, at least for GCC on Linux: the cost is extra space (for the handlers and tables) whether or not exceptions are thrown, plus the extra cost of parsing the tables and executing the handlers when an exception is thrown. If you use exceptions instead of error codes, and an error is rare, it can be faster, since you do not have the overhead of testing for errors anymore.

In case you want more information, in particular what all the __cxa_ functions do, see the original specification they came from:

这篇关于在c ++中异常如何工作(在幕后)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-20 03:51
查看更多