Funções virtuais e desempenho - C ++


125

No meu design de classe, uso classes abstratas e funções virtuais extensivamente. Tive a sensação de que 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 otimização prematura. Certo?


Como por minha resposta, eu sugiro fechar isso como duplicata de stackoverflow.com/questions/113830
Suma

possível duplicata da penalidade
Bo Persson

2
Se você estiver executando computação de alto desempenho e processamento de números, não use nenhuma virtualidade no núcleo do cálculo: ele definitivamente mata todos os desempenhos e evita otimizações em tempo de compilação. Para inicialização ou finalização do programa, isso não é importante. Ao trabalhar com interfaces, você pode usar a virtualidade como desejar.
Vincent

Respostas:


90

Uma boa regra geral é:

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

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

Um excelente artigo que fala sobre funções virtuais (e mais) são os Indicadores de Função de Membro e os Delegados de C ++ mais rápidos possíveis .


E as funções virtuais puras? Eles afetam o desempenho de alguma forma? Apenas imaginando como eles estão lá simplesmente para impor a implementação.
thomthom

2
@ thomthom: Correto, não há diferença de desempenho entre funções virtuais puras e virtuais comuns.
Greg Hewgill

168

Sua pergunta me deixou curiosa, então fui em frente e executei alguns tempos na CPU PowerPC em ordem de 3GHz com a qual trabalhamos. O teste que fiz foi criar uma classe vetorial 4d simples 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 
}

Depois, configurei três matrizes, cada uma contendo 1024 desses vetores (pequenos o suficiente para caber em L1) e executei um loop que os adicionou um ao outro (Ax = Bx + Cx) 1000 vezes. Corri isso com as funções definidas como inline, virtuale chamadas de função regulares. Aqui estão os resultados:

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

Portanto, neste caso (onde tudo se encaixa no cache), as chamadas de função virtual eram 20x mais lentas que as chamadas embutidas. Mas o que isto significa realmente? Cada viagem pelo loop causava exatamente 3 * 4 * 1024 = 12,288chamadas de função (1024 vetores vezes quatro componentes e três chamadas por adição); portanto, esses tempos representam 1000 * 12,288 = 12,288,000chamadas de função. O loop virtual demorou 92ms a mais que o loop direto, portanto, a sobrecarga adicional por chamada foi de 7 nanossegundos por função.

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

Consulte também: comparação da montagem gerada.


Mas, se forem chamados várias vezes, geralmente poderão ser mais baratos do que quando chamados apenas uma vez. Ver a minha irrelevante blog: phresnel.org/blog , os postos intitulado "Funções virtuais considerada não prejudicial", mas é claro que depende da complexidade de seus codepaths
Sebastian Mach

22
Meu teste mede um pequeno conjunto de funções virtuais chamado repetidamente. A postagem do seu blog pressupõe que o custo do tempo do código possa ser medido pela contagem de operações, mas isso nem sempre é verdade; o principal custo de um vfunc nos processadores modernos é a bolha de pipeline causada por uma imprevisibilidade da filial.
Crashworks 13/04/09

10
isso seria uma excelente referência para o gcc LTO (Link Time Optimization); tentar compilar este novamente com lto habilitado: gcc.gnu.org/wiki/LinkTimeOptimization e ver o que acontece com o fator de 20x
lurscher

1
Se uma classe tiver uma função virtual e uma função embutida, o desempenho do método não virtual também será afetado? Simplesmente pela natureza da classe ser virtual?
thomthom

4
@ thomthom Não, virtual / não virtual é um atributo por função. Uma função só precisa ser definida via vtable se estiver marcada como virtual ou se estiver substituindo uma classe base que a possui como virtual. Você verá frequentemente classes que têm um grupo de funções virtuais para interface pública e, em seguida, muitos acessadores em linha e assim por diante. (Tecnicamente, este é específico de implementação e um compilador poderia usar ponters virtuais, mesmo para funções marcadas 'inline', mas uma pessoa que escreveu tal compilador seria insano.)
Crashworks

42

Quando o Objective-C (onde todos os métodos são virtuais) é a linguagem principal do iPhone e o Java é a principal linguagem do Android, acho que é bastante seguro usar funções virtuais C ++ em nossas torres de núcleo duplo de 3 GHz.


4
Eu não tenho certeza que o iPhone é um bom exemplo de código de alto desempenho: youtube.com/watch?v=Pdk2cJpSXLg
Crashworks

13
@ Baterhworks: O iPhone não é um exemplo de código. É um exemplo de hardware - especificamente hardware lento , que é o ponto que eu estava enfatizando aqui. Se essas linguagens supostamente "lentas" forem boas o suficiente para hardware com pouca capacidade, as funções virtuais não serão um grande problema.
Chuck

52
O iPhone é executado em um processador ARM. Os processadores ARM usados ​​para iOS foram projetados para baixo MHz e baixo consumo de energia. Não há silício para previsão de ramificação na CPU e, portanto, nenhuma sobrecarga de desempenho da previsão de ramificação perde nas chamadas de função virtual. Além disso, o hardware do MHz para iOS é baixo o suficiente para que uma falta de cache não pare o processador por 300 ciclos de clock enquanto recupera dados da RAM. As falhas de cache são menos importantes em MHz mais baixos. Em resumo, não há sobrecarga no uso de funções virtuais em dispositivos iOS, mas esse é um problema de hardware e não se aplica às CPUs de desktops.
HaltingState

4
Como programador Java de longa data, recém-criado no C ++, quero acrescentar que o compilador JIT e o otimizador de tempo de execução do Java têm a capacidade de compilar, prever e até alinhar algumas funções no tempo de execução após um número predefinido de loops. No entanto, não tenho certeza se o C ++ tem esse recurso no tempo de compilação e link, porque não possui padrão de chamada em tempo de execução. Assim, em C ++, talvez seja necessário ter um pouco mais de cuidado.
Alex Suo

@AlexSuo Não tenho certeza do seu ponto? Sendo compilado, o C ++, é claro, não pode otimizar com base no que pode acontecer em tempo de execução, portanto, a previsão etc. teria que ser feita pela própria CPU ... mas bons compiladores C ++ (se instruídos) fazem um grande esforço para otimizar funções e loops muito antes tempo de execução.
underscore_d

34

Em aplicativos muito críticos de desempenho (como videogames), uma chamada de função virtual pode ser muito lenta. Com o hardware moderno, a maior preocupação com o desempenho é a falta de cache. Se os dados não estiverem no cache, poderá levar centenas de ciclos até que estejam disponíveis.

Uma chamada de função normal pode gerar uma falha no cache de instruções 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 falha no cache de dados. Em seguida, chama a função que pode resultar em uma falta no cache de instruções como uma função não virtual.

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


6
Certo, mas qualquer código (ou vtable) que é chamado repetidamente de um loop restrito (é claro) raramente sofrerá falhas de cache. Além disso, o ponteiro vtable normalmente está na mesma linha de cache que outros dados no objeto que o método chamado acessará; geralmente, estamos falando de apenas uma falta extra de cache.
Qwertie

5
@ Qwertie Eu não acho que isso seja verdade necessário. O corpo do loop (se for maior que o cache L1) pode "aposentar" vtable ponteiro, ponteiro de função e iteração posterior teria que esperar para cache L2 (ou mais) de acesso em cada iteração
Ghita

30

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

O tempo necessário para chamar uma função de membro virtual é alguns ciclos de relógio a mais do que o necessário para chamar uma função de membro não virtual, desde que a instrução de chamada de função sempre chame a mesma versão da função virtual. Se a versão mudar, você receberá uma penalidade de erro de previsão de 10 a 30 ciclos de relógio. As regras para previsão e previsão incorreta de chamadas de função virtual são as mesmas que para instruções de chave ...


Obrigado por esta referência. Os manuais de otimização da Agner Fog são o padrão-ouro para a utilização otimizada do hardware.
Arto Bendiken 27/03

Com base na minha lembrança e em uma pesquisa rápida - stackoverflow.com/questions/17061967/c-switch-and-jump-tables - duvido que isso sempre seja verdade switch. Com casevalores totalmente arbitrários , com certeza. Mas se todos os cases forem consecutivos, um compilador poderá otimizar isso em uma tabela de salto (ah, isso me lembra os bons e velhos dias do Z80), que deve ser (por falta de um termo melhor) tempo constante. Não que eu recomende tentar substituir vfuncs por switch, o que é ridículo. ;)
underscore_d

7

absolutamente. Era um problema quando os computadores rodavam a 100Mhz, já que toda chamada de método exigia uma consulta na vtable antes de ser chamada. Mas hoje .. em uma CPU 3Ghz que possui cache de 1º nível com mais memória do que meu primeiro computador tinha? De modo nenhum. Alocar memória da RAM principal custará mais tempo do que se todas as suas funções fossem virtuais.

É como nos 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 em que me incomodo 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!

O PS pensa em outras linguagens 'fáceis de usar' - todos os seus métodos são virtuais e escondidos atualmente.


4
Bem, até hoje, evitar chamadas de função é importante para aplicativos de alto desempenho. A diferença é que os compiladores de hoje incorporam funções pequenas de maneira confiável, para que não soframos penalidades de velocidade por escrever funções pequenas. Quanto às funções virtuais, as CPUs inteligentes podem fazer a previsão de ramificação inteligente nelas. Acho que o fato de os computadores antigos serem mais lentos não é realmente o problema - sim, eles eram muito mais lentos, mas naquela época sabíamos disso, por isso lhes damos cargas de trabalho muito menores. Em 1992, se tocássemos um MP3, sabíamos que poderíamos ter que dedicar mais da metade da CPU a essa tarefa.
Qwertie

6
O mp3 data de 1995. em 92, mal tínhamos 386, de jeito nenhum eles podiam tocar um mp3, e 50% do tempo da CPU supõe um bom sistema operacional multi-tarefas, um processo inativo e um agendador preventivo. Nada disso existia no mercado consumidor na época. era 100% a partir do momento em que a energia estava ligada, no final da história.
v.oddou

7

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: ATL usa " ligação dinâmica simulada " em tempo de compilação com modelosobter o efeito do "polimorfismo estático", que é meio difícil de explicar; você basicamente passa a classe derivada como parâmetro para um modelo de classe base; portanto, em tempo de compilação, a classe base "sabe" qual é sua classe derivada em cada instância. Não permitirá que você armazene várias classes derivadas diferentes em uma coleção de tipos de base (isso é polimorfismo em tempo de execução), mas de um sentido estático, se você quiser criar uma classe Y igual a uma classe de modelo X preexistente, que possui o ganchos para esse tipo de substituição, você só precisa substituir os métodos mais importantes e, em seguida, obter os métodos básicos da classe X sem ter que ter uma tabela v.

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

Veja também esta outra questão SO .

A propósito, aqui está uma postagem que descobri que fala sobre os aspectos de desempenho no tempo da CPU.



4

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


1
O artigo vinculado não considera parte muito importante da chamada virtual, e isso é possível imprecisão de ramificação.
Suma

4

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

Embora, como outras pessoas tenham dito, nunca seja um problema para você na vida real. E se você acha que é, execute um criador de perfil, faça alguns testes e verifique se isso realmente é um problema antes de tentar "cancelar a designação" do seu código para obter um benefício de desempenho.


2
chamando qualquer coisa dentro de um loop é provável que manter todo o código e os dados quente no cache ...
Greg Rogers

2
Sim, mas se esse loop correto estiver repetindo uma lista de objetos, cada objeto poderá estar chamando uma função virtual em um endereço diferente através da mesma chamada de função.
Daemin

3

Quando o método de classe não é virtual, o compilador geralmente faz alinhamento. Por outro lado, 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 uma quantidade significativa de trabalho dentro da função - isso pode ser insignificante.

Ou, quando é uma chamada virtual usada repetidamente várias vezes, enquanto faz alguma operação simples - pode ser realmente grande.


4
Uma chamada de função virtual é cara em comparação com ++ia. E daí?
Bo Persson

2

Eu andei de um lado para o outro pelo menos 20 vezes em meu projeto em particular. Embora não pode haver algumas grandes ganhos em termos de reutilização de código, clareza, facilidade de manutenção e legibilidade, por outro lado, sucessos de desempenho ainda fazer existir com funções virtuais.

O impacto no desempenho será notado em um laptop / desktop / tablet moderno ... provavelmente não! No entanto, em certos casos com sistemas embarcados, o impacto no desempenho pode ser o fator determinante na ineficiência do seu código, especialmente se a função virtual for chamada repetidamente 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 do uso de uma determinada construção em detrimento de outra. A menos que você seja super orientado para o desempenho, provavelmente não se importa com o impacto no desempenho e deve usar todo o material OO em C ++ para ajudar a tornar seu código o mais utilizável possível.


2

Na minha experiência, o principal aspecto relevante é a capacidade de alinhar uma função. Se você tiver necessidades de desempenho / otimização que determinam uma função, você não poderá tornar a função virtual porque isso impediria. Caso contrário, você provavelmente não notará a diferença.


1

Uma coisa a notar é que isto:

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 "may" porque isso depende do compilador, do cache etc.


0

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


2
nunca, por menor que seja o computador de destino?
zumalifeguard

Eu poderia ter concordado se você tivesse formulado isso como The performance penalty of using virtual functions can sometimes be so insignificant that it is completely outweighed by the advantages you get at the design level.a principal diferença está dizendo sometimes, não never.
Underscore_d

-1

Eu sempre me questionei isso, especialmente porque - há alguns anos atrás - eu também fiz esse teste comparando os tempos de uma chamada de método de membro padrão com uma virtual e fiquei realmente irritado com os resultados naquele momento, tendo 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 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 fiquei realmente surpreso que isso - de fato - realmente não importa mais. Embora faça sentido ter inline mais rapidamente do que os não virtuais, e eles sendo mais rápidos que os virtuais, geralmente chega à carga do computador em geral, se seu cache tem os dados necessários ou não, e embora você possa otimizar no nível do cache, eu acho, que isso deve ser feito pelos desenvolvedores do compilador mais do que pelos desenvolvedores de aplicativos.


12
Eu acho que é bem provável que seu compilador possa dizer que a chamada de função virtual no seu código pode chamar apenas Virtual :: call. Nesse caso, ele pode simplesmente incorporá-lo. Também não há nada que impeça o compilador de incluir a chamada Normal ::, mesmo que você não tenha solicitado. Então eu acho que é bem possível que você obtenha os mesmos horários para as 3 operações, porque o compilador está gerando código idêntico para elas.
Bjarke H. Roune
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.