c++ - virtuais - poo virtual




Funções virtuais e desempenho-C++ (10)

A única maneira de ver que uma função virtual se tornará um problema de desempenho é se muitas funções virtuais forem chamadas dentro de um loop apertado e se, e somente se , causarem uma falha de página ou outra operação de memória "pesada".

Embora, como outras pessoas disseram, praticamente nunca será um problema para você na vida real. E se você acha que é, execute um profiler, faça alguns testes e verifique se isso realmente é um problema antes de tentar "desesignar" seu código para obter um benefício de desempenho.

No design da minha classe, eu uso classes abstratas e funções virtuais extensivamente. Eu tive a sensação de que as funções virtuais afetam o desempenho. Isso é verdade? Mas acho que essa diferença de desempenho não é perceptível e parece que estou fazendo uma otimização prematura. Certo?


A partir da página 44 do manual "Otimizando o software em C ++" da Agner Fog :

O tempo que leva para chamar uma função de membro virtual é alguns ciclos de clock mais do que leva para chamar uma função de membro não-virtual, desde que a instrução de chamada de função sempre chama a mesma versão da função virtual. Se a versão mudar, você receberá uma penalidade por erro de 10 a 30 ciclos de clock. As regras para previsão e erro de chamadas de função virtual são as mesmas que para as declarações switch ...


Em aplicações críticas de desempenho (como videogames), uma chamada de função virtual pode ser muito lenta. Com hardware moderno, a maior preocupação de desempenho é o cache miss. Se os dados não estiverem no cache, pode haver centenas de ciclos antes de estarem disponíveis.

Uma chamada de função normal pode gerar um erro de cache de instrução quando a CPU busca a primeira instrução da nova função e ela não está no cache.

Uma chamada de função virtual precisa primeiro carregar o ponteiro vtable do objeto. Isso pode resultar em uma falha no cache de dados. Em seguida, ele carrega o ponteiro de função da vtable, o que pode resultar em outra falta de cache de dados. Em seguida, ele chama a função que pode resultar em uma falha de cache de instrução como uma função não virtual.

Em muitos casos, duas falhas de cache extras não são uma preocupação, mas em um loop rígido no código crítico de desempenho, ele pode reduzir drasticamente o desempenho.


Eu fui de um lado para outro sobre isso pelo menos 20 vezes no meu projeto em particular. Embora possa haver alguns grandes ganhos em termos de reutilização de código, clareza, capacidade de manutenção e legibilidade, por outro lado, os hits de desempenho ainda existem com funções virtuais.

Será que o desempenho será notado em um laptop / desktop / tablet moderno ... provavelmente não! No entanto, em determinados casos com sistemas incorporados, o impacto no desempenho pode ser o fator determinante na ineficiência do seu código, especialmente se a função virtual for chamada repetidas vezes em um loop.

Aqui está um artigo datado que analisa as práticas recomendadas para C / C ++ no contexto de sistemas embarcados: http://www.open-std.org/jtc1/sc22/wg21/docs/ESC_Boston_01_304_paper.pdf

Para concluir: cabe ao programador entender os prós / contras de usar um determinado construto em detrimento de outro. A menos que você seja super guiado pelo desempenho, você provavelmente não se importa com o desempenho e deve usar todas as coisas OO em C ++ para ajudar a tornar seu código o mais possível.


Há outro critério de desempenho além do tempo de execução. Uma Vtable também ocupa espaço de memória e, em alguns casos, pode ser evitada: A ATL usa " ligação dinâmica simulada " em tempo de compilação com templates para obter o efeito de "polimorfismo estático", o que é difícil de explicar; Basicamente, você passa a classe derivada como um parâmetro para um modelo de classe base, portanto, em tempo de compilação, a classe base "sabe" qual é a classe derivada em cada instância. Não permite armazenar várias classes derivadas diferentes em uma coleção de tipos de base (polimorfismo de tempo de execução), mas de um sentido estático, se você quiser fazer uma classe Y que seja igual a uma classe de modelo X preexistente que tenha a classe ganchos para esse tipo de substituição, você só precisa sobrescrever os métodos com os quais se importa, e então você obtém os métodos base da classe X sem ter que ter uma vtable.

Em classes com grandes pegadas de memória, o custo de um único ponteiro vtable não é muito, mas algumas das classes ATL em COM são muito pequenas e vale a pena economizar vtable se o caso de polimorfismo em tempo de execução nunca ocorrer.

Veja também esta outra questão SO .

Aliás, aqui está uma postagem que encontrei sobre os aspectos de desempenho do tempo da CPU.


Na minha experiência, a principal coisa relevante é a capacidade de inline uma função. Se você tem necessidades de desempenho / otimização que determinam que uma função precisa ser embutida, você não pode tornar a função virtual porque evitaria isso. Caso contrário, você provavelmente não notará a diferença.


Quando o método de classe não é virtual, o compilador geralmente faz alinhamentos. Ao contrário, quando você usa o ponteiro para alguma classe com função virtual, o endereço real será conhecido apenas no tempo de execução.

Isso é bem ilustrado pelo teste, diferença de tempo ~ 700% (!):

#include <time.h>

class Direct
{
public:
    int Perform(int &ia) { return ++ia; }
};

class AbstrBase
{
public:
    virtual int Perform(int &ia)=0;
};

class Derived: public AbstrBase
{
public:
    virtual int Perform(int &ia) { return ++ia; }
};


int main(int argc, char* argv[])
{
    Direct *pdir, dir;
    pdir = &dir;

    int ia=0;
    double start = clock();
    while( pdir->Perform(ia) );
    double end = clock();
    printf( "Direct %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    Derived drv;
    AbstrBase *ab = &drv;

    ia=0;
    start = clock();
    while( ab->Perform(ia) );
    end = clock();
    printf( "Virtual: %.3f, ia=%d\n", (end-start)/CLOCKS_PER_SEC, ia );

    return 0;
}

O impacto da chamada de função virtual depende muito da situação. Se houver poucas chamadas e quantidade significativa de trabalho dentro da função - pode ser insignificante.

Ou, quando se trata de uma chamada virtual usada repetidamente várias vezes, durante uma operação simples, ela pode ser muito grande.


Sim, você está certo e se você curioso sobre o custo da chamada de função virtual, você pode achar este post interessante.


Uma boa regra é:

Não é um problema de desempenho até que você possa provar isso.

O uso de funções virtuais terá um efeito muito pequeno no desempenho, mas é improvável que afete o desempenho geral do seu aplicativo. Locais melhores para procurar melhorias de desempenho são em algoritmos e E / S.

Um excelente artigo que fala sobre funções virtuais (e mais) é Ponteiros de Função de Membro e os Delegados de C ++ Mais Rápidos Possíveis .


Uma coisa a notar é que isso:

boolean contains(A element) {
    for (A current: this)
        if (element.equals(current))
            return true;
    return false;
}

pode ser mais rápido que isso:

boolean contains(A element) {
    for (A current: this)
        if (current.equals(equals))
            return true;
    return false;
}

Isso ocorre porque o primeiro método está chamando apenas uma função, enquanto o segundo pode estar chamando muitas funções diferentes. Isso se aplica a qualquer função virtual em qualquer idioma.

Eu digo "pode" porque isso depende do compilador, do cache etc.





virtual-functions