vinculação Funções virtuais e desempenho-C++




poo virtual (12)

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?


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


Eu sempre me questionei sobre isso, especialmente porque - alguns anos atrás - eu também fiz um teste comparando os timings de uma chamada de método de membro padrão com uma virtual e fiquei muito bravo com os resultados da época, com chamadas virtuais vazias sendo 8 vezes mais lento que os não virtuais.

Hoje eu tive que decidir se deveria ou não usar uma função virtual para alocar mais memória na minha classe de buffer, em um aplicativo muito crítico para o desempenho, então eu pesquisei (e encontrei você) e, no final, fiz o teste novamente.

// g++ -std=c++0x -o perf perf.cpp -lrt
#include <typeinfo>    // typeid
#include <cstdio>      // printf
#include <cstdlib>     // atoll
#include <ctime>       // clock_gettime

struct Virtual { virtual int call() { return 42; } }; 
struct Inline { inline int call() { return 42; } }; 
struct Normal { int call(); };
int Normal::call() { return 42; }

template<typename T>
void test(unsigned long long count) {
    std::printf("Timing function calls of '%s' %llu times ...\n", typeid(T).name(), count);

    timespec t0, t1;
    clock_gettime(CLOCK_REALTIME, &t0);

    T test;
    while (count--) test.call();

    clock_gettime(CLOCK_REALTIME, &t1);
    t1.tv_sec -= t0.tv_sec;
    t1.tv_nsec = t1.tv_nsec > t0.tv_nsec
        ? t1.tv_nsec - t0.tv_nsec
        : 1000000000lu - t0.tv_nsec;

    std::printf(" -- result: %d sec %ld nsec\n", t1.tv_sec, t1.tv_nsec);
}

template<typename T, typename Ua, typename... Un>
void test(unsigned long long count) {
    test<T>(count);
    test<Ua, Un...>(count);
}

int main(int argc, const char* argv[]) {
    test<Inline, Normal, Virtual>(argc == 2 ? atoll(argv[1]) : 10000000000llu);
    return 0;
}

E ficou realmente surpreso com o fato de que, na verdade, realmente não importa mais. Embora faça sentido ter mais inlines do que os não virtuais, e eles serem mais rápidos do que os virtuais, isso geralmente acarreta a carga do computador como um todo, independentemente de seu cache ter os dados necessários ou não, e embora você possa otimizar no nível do cache, eu acho, isso deve ser feito pelos desenvolvedores do compilador mais do que pelos desenvolvedores do aplicativo.


absolutamente. Foi um problema quando os computadores rodavam a 100Mhz, já que toda chamada de método requeria uma pesquisa na vtable antes de ser chamada. Mas hoje .. em um processador de 3 GHz que tem cache de nível 1 com mais memória do que o meu primeiro computador? De modo nenhum. A alocação de memória da RAM principal custa mais tempo do que se todas as suas funções fossem virtuais.

É como nos velhos e velhos tempos em que as pessoas diziam que a programação estruturada era lenta porque todo o código era dividido em funções, cada função exigia alocações de pilha e uma chamada de função!

A única vez que eu sequer pensaria em me preocupar em considerar o impacto no desempenho de uma função virtual, é se ela foi muito usada e instanciada em código de modelo que acabou em tudo. Mesmo assim, eu não gastaria muito esforço nisso!

PS pensar em outras linguagens 'fáceis de usar' - todos os seus métodos são virtuais e eles não engatinham hoje em dia.


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.


Sua pergunta me deixou curiosa, então segui em frente e executei alguns timings no processador PowerPC 3GHz em ordem com o qual trabalhamos. O teste que eu fiz foi fazer uma simples classe vetorial 4d com funções get / set

class TestVec 
{
    float x,y,z,w; 
public:
    float GetX() { return x; }
    float SetX(float to) { return x=to; }  // and so on for the other three 
}

Então eu configurei três arrays, cada um contendo 1024 desses vetores (pequeno o suficiente para caber em L1) e executei um loop que os adicionou um ao outro (Ax = Bx + Cx) 1000 vezes. Eu corri isso com as funções definidas como chamadas de função inline , virtual e regular. Aqui estão os resultados:

  • inline: 8ms (0,65ns por chamada)
  • direto: 68ms (5,53ns por chamada)
  • virtual: 160ms (13ns por chamada)

Portanto, nesse caso (onde tudo se encaixa no cache), as chamadas de função virtual eram cerca de 20x mais lentas que as chamadas in-line. Mas o que isto significa realmente? Cada trip através do loop causou exatamente 3 * 4 * 1024 = 12,288 chamadas de função (1024 vetores vezes quatro componentes vezes três chamadas por adição), então esses tempos representam 1000 * 12,288 = 12,288,000 chamadas de função. O loop virtual levou 92ms a mais do que o loop direto, de modo que a sobrecarga adicional por chamada foi de 7 nanossegundos por função.

A partir disso, concluo: sim , as funções virtuais são muito mais lentas que as funções diretas, e não , a menos que você esteja planejando chamá-las dez milhões de vezes por segundo, não importa.

Veja também: comparação do conjunto gerado.


A penalidade de desempenho de usar funções virtuais nunca pode outweight as vantagens que você obtém no nível de design. Supostamente, uma chamada para uma função virtual seria 25% menos eficiente do que uma chamada direta para uma função estática. Isso ocorre porque existe um nível de indireção através do VMT. No entanto, o tempo necessário para fazer a chamada é normalmente muito pequeno em comparação com o tempo gasto na execução real de sua função, de modo que o custo total de desempenho será insignificante, especialmente com o desempenho atual do hardware. Além disso, o compilador pode às vezes otimizar e ver que nenhuma chamada virtual é necessária e compilá-lo em uma chamada estática. Portanto, não se preocupe, use funções virtuais e classes abstratas tanto quanto você precisar.


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 .


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.


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.


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.


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


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.







virtual-functions