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") é:
- Copie alguns registros na pilha, para permitir que a função chamada use esses registros.
- Copie os argumentos em locais predefinidos, para que a função chamada possa encontrá-los, independentemente de onde é chamada.
- Empurre o endereço de retorno.
- Ramifique / salte para o código da função, que é um endereço em tempo de compilação e, portanto, codificado no binário pelo compilador / vinculador.
- Obtenha o valor de retorno de um local predefinido e restaure os registros que queremos usar.
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 ...
- Obtenha o ponteiro vtable, que aponta para uma matriz de ponteiros de função (endereços de função), um para cada função virtual, do objeto.
- Obtenha o endereço de função correto da vtable em um registrador (o índice em que o endereço de função correto está armazenado é decidido em tempo de compilação).
- Vá para o endereço desse registro, em vez de saltar para um endereço codificado.
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.