Pergunta

Eu continuo vendo as pessoas dizem que as exceções são lentos, mas eu nunca vi nenhuma prova. Então, em vez de perguntar se eles são, vou perguntar como fazer exceções trabalho por trás da cena, para que eu possa fazer um decisões de quando usá-los e se eles são lentos.

Pelo que eu sei, as exceções são a mesma coisa que fazer um monte de retorno, mas ele também verifica quando ele precisa parar de fazer o retorno. Como ele se verificar quando fazer parar? Estou tendo um palpite e dizer que há uma segunda pilha que detém o tipo de exceção e localização pilha, em seguida, faz retornos até que ele chega lá. Eu também estou supondo que a única vez que a pilha é o toque está em um lance e cada try / catch. AFAICT implementação de um comportamento semelhante com código de retorno tomaria a mesma quantidade de tempo. Mas tudo isso é um palpite, então eu quero saber.

Como fazer exceções realmente funciona?

Foi útil?

Solução

Em vez de adivinhar, eu decidi realmente olhar para o código gerado com um pequeno pedaço de código C ++ e instalar um pouco velho 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);
}

Eu compilei com g++ -m32 -W -Wall -O3 -save-temps -c, e olhou para o arquivo de montagem gerado.

    .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(), para que o compilador decidiu que precisava de uma cópia não-inline do destruidor.

.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! Não há instruções extras em todos no caminho de código normal. O compilador em vez gerado blocos adicionais para fora-de-linha de código de correcção, referenciadas através de uma mesa, no final da função (que é efectivamente colocado sobre uma secção separada do executável). Todo o trabalho é feito nos bastidores pela biblioteca padrão, com base nessas tabelas (_ZTI11MyException é typeinfo for MyException).

OK, isso não era realmente uma surpresa para mim, eu já sabia como este compilador fez isso. Continuando com a saída de montagem:

    .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

Aqui vemos o código para lançar uma exceção. Enquanto não havia nenhum extra sobrecarga, simplesmente porque uma exceção pode ser lançada, há obviamente uma grande quantidade de sobrecarga em realmente jogar e pegar uma exceção. A maior parte é escondida dentro __cxa_throw, que deverá:

  • Caminhar a pilha com a ajuda das tabelas de exceção até encontrar um manipulador para essa exceção.
  • Unwind a pilha até que chegue a esse manipulador.
  • Na verdade, chamar o manipulador.

Compare isso com o custo de simplesmente devolver um valor, e você ver por que as exceções devem ser usados ??apenas para retornos excepcionais.

Para terminar, o resto do arquivo de montagem:

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

O typeinfo dados.

    .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
manipulação

Ainda mais exceção tabelas e informação extra assorted.

Assim, a conclusão, pelo menos para GCC em Linux: o custo é de espaço extra (para os manipuladores e tabelas) ou não exceções são lançadas, mais o custo extra de analisar as tabelas e executar as rotinas de tratamento quando uma exceção é lançada. Se você usar exceções em vez de códigos de erro, e um erro é raro, pode ser mais rápido , desde que você não tem a sobrecarga de testar para erros mais.

No caso de você quiser obter mais informações, em particular o que todas as funções __cxa_ fazer, ver a especificação original de onde vieram:

Outras dicas

Exceções ser lento foi true nos velhos tempos.
Na maioria compilador moderna esta já não é válido.

Nota: Só porque temos exceções não significa que nós não usar códigos de erro também. Quando erro pode ser tratada localmente usar códigos de erro. Quando os erros requerem mais contexto para exceções de uso de correção: Eu escrevi muito mais eloquentemente aqui: Quais são os princípios orientadores sua exceção política de manipulação?

O custo de manipulação de exceção código quando há exceções estão sendo usados ??é praticamente zero.

Quando uma exceção é lançada há algum trabalho feito.
Mas você tem que comparar este com o custo de retornar códigos de erro e verificá-los por todo o caminho de volta para o ponto onde o erro pode ser tratado. Tanto mais demorado para escrever e manter.

Além disso, há uma pegadinha para os novatos:
Apesar de objetos de exceção é suposto ser pequena algumas pessoas colocar um monte de coisas dentro deles. Então você tem o custo de copiar o objeto de exceção. A solução não é duas vezes:

  • Não coloque material extra em sua exceção.
  • Captura por referência const.

Na minha opinião eu apostaria que o mesmo código com exceções é tanto mais eficiente ou pelo menos tão comparáveis ??quanto o código sem as exceções (mas tem todo o código extra para verificar os resultados de erro de função). Lembre-se você não está recebendo nada de graça o compilador está gerando o código que você deveria ter escrito, em primeiro lugar para verificar códigos de erro (e normalmente o compilador é muito mais eficiente do que um ser humano).

Há uma série de maneiras que você pode implementar exceções, mas geralmente eles vão contar com algum apoio subjacente do sistema operacional. No Windows este é o mecanismo estruturado manipulação de exceção.

Há uma discussão decente dos detalhes sobre projeto de código: Como um tratamento de exceção C ++ implementos compilador

A sobrecarga de excepções ocorre porque o compilador tem para gerar o código para manter o controle da qual os objectos devem ser destruídos em cada quadro da pilha (ou mais precisamente escopo) se propaga uma excepção de que âmbito. Se uma função não tem variáveis ??locais na pilha que exigem destruidores de ser chamado, então não deve ter um tratamento de perda de desempenho wrt exceção.

Usando um código de retorno só pode descontrair um único nível da pilha de cada vez, enquanto que uma exceção mecanismo de tratamento pode saltar muito mais para trás para baixo da pilha em uma operação se não há nada para ele fazer nos quadros de pilha intermediários.

Matt Pietrek escreveu um excelente artigo sobre Win32 Structured Exception Handling . Embora este artigo foi originalmente escrito em 1997, ainda se aplica hoje (mas é claro que só se aplica a Windows).

Este artigo examina a questão e, basicamente, descobre que na prática existe um run- custo hora de exceções, embora o custo é relativamente baixo se a exceção não é lançada. Bom artigo, recomendado.

Um amigo me escreveu um pouco como o Visual C ++ trata exceções há alguns anos.

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

Todas as boas respostas.

Além disso, pensar sobre o quanto mais fácil é para depurar o código que faz 'se' cheques como portões no topo de métodos em vez de permitir que o código para lançar exceções.

Meu lema é que é fácil escrever código que funciona. A coisa mais importante é escrever o código para a próxima pessoa que olha para ele. Em alguns casos, é você em 9 meses, e você não quer ser xingando seu nome!

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top