Alguém pode explicar em detalhes como exatamente a tabela virtual funciona e quais ponteiros estão associados quando as funções virtuais são chamadas.
Se eles forem realmente mais lentos, você pode mostrar que o tempo que a função virtual leva para executar é mais do que os métodos de classe normais? É fácil perder a noção de como / o que está acontecendo sem ver algum código.
Respostas:
Os métodos virtuais são comumente implementados por meio das chamadas tabelas de métodos virtuais (vtable for short), nas quais os ponteiros de função são armazenados. Isso adiciona indireção à chamada real (precisa buscar o endereço da função a ser chamada na vtable e depois chamá-la - em vez de apenas chamá-la imediatamente). Obviamente, isso leva algum tempo e um pouco mais de código.
No entanto, não é necessariamente a principal causa de lentidão. O verdadeiro problema é que o compilador (geralmente / geralmente) não pode saber qual função será chamada. Portanto, não pode incorporá-lo ou executar outras otimizações. Isso por si só pode adicionar uma dúzia de instruções inúteis (preparar registros, chamar e restaurar o estado posteriormente) e pode inibir outras otimizações aparentemente não relacionadas. Além disso, se você ramificar como louco chamando muitas implementações diferentes, sofrerá os mesmos hits de ramificar como louco por outros meios: o preditor de cache e ramificação não o ajudará, os ramos levarão mais tempo do que um perfeitamente previsível ramo.
Grande, mas : esses resultados de desempenho geralmente são muito pequenos para importar. Vale a pena considerar se você deseja criar um código de alto desempenho e considerar adicionar uma função virtual que seria chamada com frequência alarmante. No entanto, também ter em mente que a substituição de chamadas de funções virtuais com outros meios de ramificação (
if .. else
,switch
, ponteiros de função, etc.) não vai resolver a questão fundamental - ele pode muito bem ser mais lento. O problema (se é que existe) não são funções virtuais, mas indiretas (desnecessárias).Editar: a diferença nas instruções de chamada é descrita em outras respostas. Basicamente, o código para uma chamada estática ("normal") é:
Uma chamada virtual faz exatamente a mesma coisa, exceto que o endereço da função não é conhecido no momento da compilação. Em vez disso, algumas instruções ...
Quanto aos ramos: Um ramo é qualquer coisa que salta para outra instrução em vez de apenas deixar a próxima instrução executar. Isto inclui
if
,switch
, partes de vários loops, chamadas de função, etc, e às vezes os implementos compilador coisas que não parecem ramo de uma forma que realmente precisa de um ramo sob o capô. Consulte Por que o processamento de uma matriz classificada é mais rápido que uma matriz não classificada? por que isso pode ser lento, o que as CPUs fazem para combater essa desaceleração e como isso não é uma solução definitiva.fonte
virtual
.Aqui estão alguns códigos desmontados reais de uma chamada de função virtual e uma chamada não virtual, respectivamente:
Você pode ver que a chamada virtual requer três instruções adicionais para procurar o endereço correto, enquanto o endereço da chamada não virtual pode ser compilado.
No entanto, observe que na maioria das vezes esse tempo extra de pesquisa pode ser considerado insignificante. Em situações em que o tempo de pesquisa seria significativo, como em um loop, o valor geralmente pode ser armazenado em cache executando as três primeiras instruções antes do loop.
A outra situação em que o tempo de pesquisa se torna significativo é se você tiver uma coleção de objetos e estiver repetindo a chamada de uma função virtual em cada um deles. No entanto, nesse caso, você precisará de alguns meios para selecionar qual função chamar de qualquer maneira, e uma consulta à tabela virtual é o melhor meio possível. De fato, como o código de pesquisa da vtable é tão amplamente usado, ele é altamente otimizado, portanto, tentar contorná-lo manualmente tem uma boa chance de resultar em pior desempenho.
fonte
-0x8(%rbp)
. oh meu ... essa sintaxe da AT&T.Mais lento que o que ?
As funções virtuais resolvem um problema que não pode ser resolvido por chamadas diretas de função. Em geral, você pode comparar apenas dois programas que calculam a mesma coisa. "Esse traçador de raios é mais rápido que o compilador" não faz sentido, e esse princípio generaliza até coisas pequenas, como funções individuais ou construções de linguagem de programação.
Se você não usar uma função virtual para alternar dinamicamente para um pedaço de código com base em um dado, como o tipo de um objeto, precisará usar outra coisa, como uma
switch
instrução para realizar a mesma coisa. Essa outra coisa tem suas próprias despesas gerais, além de implicações na organização do programa que influenciam sua capacidade de manutenção e desempenho global.Observe que, em C ++, chamadas para funções virtuais nem sempre são dinâmicas. Quando chamadas são feitas em um objeto cujo tipo exato é conhecido (porque o objeto não é um ponteiro ou referência, ou porque seu tipo pode ser inferido estaticamente), as chamadas são apenas chamadas regulares de função de membro. Isso não significa apenas que não há despesas gerais de envio, mas também que essas chamadas podem ser incorporadas da mesma maneira que as chamadas comuns.
Em outras palavras, seu compilador C ++ pode funcionar quando as funções virtuais não exigem expedição virtual, portanto, geralmente não há motivo para se preocupar com o desempenho delas em relação às funções não virtuais.
Novo: Além disso, não devemos esquecer as bibliotecas compartilhadas. Se você estiver usando uma classe que está em uma biblioteca compartilhada, a chamada para uma função de membro comum não será simplesmente uma sequência de instruções agradável
callq 0x4007aa
. Ele precisa passar por algumas dificuldades, como indiretamente, através de uma "tabela de links de programas" ou alguma estrutura desse tipo. Portanto, a indireção da biblioteca compartilhada pode nivelar um pouco (se não completamente) a diferença de custo entre a chamada virtual (verdadeiramente indireta) e uma chamada direta. Portanto, o raciocínio sobre as trocas de funções virtuais deve levar em consideração como o programa é construído: se a classe do objeto de destino está monoliticamente vinculada ao programa que está fazendo a chamada.fonte
porque uma chamada virtual é equivalente a
onde, com uma função não virtual, o compilador pode dobrar constantemente a primeira linha, isso é uma desreferência, uma adição e uma chamada dinâmica transformada em apenas uma chamada estática
isso também permite incorporar a função (com todas as conseqüências de otimização devidas)
fonte