Qual é o custo de usar um ponteiro para função de membro vs. um interruptor?

StackOverflow https://stackoverflow.com/questions/113150

  •  02-07-2019
  •  | 
  •  

Pergunta

Eu tenho a seguinte situação:


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

Na minha implementação atual salvar o valor de whichFoo em um membro de dados no construtor I e usar um switch em callFoo() para decidir qual dos do foo para chamar. Alternativamente, posso usar um switch no construtor para salvar um ponteiro para a fooN() direito de ser chamado em callFoo().

A minha pergunta é qual o caminho é mais eficiente se um objeto da classe A, só é construída uma vez, enquanto callFoo() é chamado um número muito grande de vezes. Assim, no primeiro caso, temos várias execuções de uma instrução switch, enquanto no segundo há apenas um switch, e várias chamadas de uma função membro usando o ponteiro para ele. Eu sei que chamar uma função membro usando um ponteiro é mais lento do que apenas chamá-lo diretamente. Alguém sabe se essa sobrecarga é mais ou menos do que o custo de um switch?

Esclarecimento: eu percebo que você nunca sabe qual abordagem dá melhor desempenho até que você experimentá-lo e tempo. No entanto, neste caso, eu já tenho abordagem 1 implementada, e eu queria saber se abordagem 2 pode ser mais eficiente, pelo menos em princípio. Parece que ele pode ser, e agora faz sentido para mim que se preocupar para implementá-lo e experimentá-lo.

Oh, e eu também gosto abordagem 2 melhor por razões estéticas. Eu acho que eu estou procurando uma justificativa para implementá-lo. :)

Foi útil?

Solução

Como você está certo de que chamar uma função membro através de um ponteiro é mais lento do que apenas chamando-o diretamente? você pode medir a diferença?

Em geral, você não deve confiar em sua intuição ao fazer avaliações de desempenho. Sente-se com seu compilador e uma função de tempo, e realmente medida as diferentes opções. Você pode ser surpreendido!

Mais informações: Há um excelente artigo Delegados função de membro ponteiros ea mais rápida possível C ++ que vai para muito profundo detalhes sobre a implementação de ponteiros de função de membro.

Outras dicas

Você pode escrever isso:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

O cálculo do ponteiro de função real é linear e rápida:

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

Note que você não tem sequer a fixar o número real função no construtor.

Eu comparei este código para o asm gerado por um switch. A versão switch não fornece qualquer aumento de desempenho.

Para responder à pergunta:. No nível com granulação mais fina, o ponteiro para a função de membro terá melhor desempenho

Para resolver a pergunta não formulada: o que significa "melhor" significa aqui? Na maioria dos casos eu esperaria a diferença para ser insignificante. Dependendo do que a classe que ele está fazendo, no entanto, a diferença pode ser significativa. O teste de desempenho antes de se preocupar com a diferença é, obviamente, o primeiro passo certo.

Se você estiver indo para continuar usando um interruptor, o que é perfeitamente bem, então você provavelmente deve colocar a lógica em um método auxiliar e chamada se a partir do construtor. Alternativamente, este é um caso clássico do Estratégia Padrão . Você poderia criar uma interface (ou classe abstrata) chamado IFoo que tem um método com a assinatura de Foo. Você teria a tomada construtor em uma instância do IFoo ( construtor Dependência Injection que implementou o método foo que você deseja. você teria um IFoo privada que seria criado com este construtor, e cada vez que você queria chamar Foo você chamaria a versão do seu IFoo.

Nota:. Eu não trabalhei com C ++ desde a faculdade, então minha linguagem pode estar fora aqui, ut as idéias gerais segure por maioria de linguagens OO

Se o seu exemplo é o código real, então eu acho que você deve rever seu projeto de classe. Passando um valor para o construtor, e usando isso para mudar o comportamento é realmente equivalente à criação de uma subclasse. Considere refatoração para torná-lo mais explícito. O efeito de fazê-lo é que seu código vai acabar usando um ponteiro de função (todos os métodos virtuais são, realmente, são ponteiros de função em tabelas de salto).

Se, no entanto o seu código era apenas um exemplo simplificado para perguntar se, em geral, tabelas de salto são mais rápidas que declarações switch, então minha intuição dizia que as tabelas de salto são mais rápidos, mas você é dependente de etapa de otimização do compilador. Mas se o desempenho é realmente uma preocupação, nunca confiar na intuição -. Bater-se um programa de teste e testá-lo, ou olhar para a montador gerado

Uma coisa é certa, uma instrução switch nunca será mais lento do que uma mesa de salto. A razão é que optimiser o melhor que um compilador pode fazer vai ser muito virar uma série de testes condicionais (ou seja, um switch) em uma tabela de salto. Então se você realmente quer ter a certeza, ter o compilador fora do processo de decisão e usar uma tabela de salto.

Parece que você deve fazer callFoo uma função virtual pura e criar algumas subclasses de A.

A menos que você realmente precisa da velocidade, ter feito uma extensa perfis e instrumentação, e determinou que as chamadas para callFoo são realmente o gargalo. E você?

Ponteiros de função são quase sempre melhor do que acorrentadas-ifs. Eles fazem um código mais limpo, e são quase sempre mais rápido (exceto talvez em um caso em que seu apenas uma escolha entre duas funções e está sempre previu corretamente).

Eu deveria pensar que o ponteiro seria mais rápido.

instruções de pré-busca Modern CPUs; mis-previu ramos liberar o cache, o que significa que barracas enquanto ele preenche o cache. A doens't ponteiro fazer isso.

É claro, você deve medir ambos.

Optimize somente quando necessário

Primeiro: Na maioria das vezes você provavelmente não se importam, a diferença será muito pequena. Certifique-se de otimizar esta chamada realmente faz sentido em primeiro lugar. Só se suas medições mostram que há tempo realmente significativo gasto na chamada sobrecarga, avance para otimizá-lo (plug descarado - Cf. Como otimizar um aplicativo para torná-lo mais rápido? ) Se a otimização não é significativo, preferem o código mais legível.

custo da chamada indireta depende de plataforma de destino

Depois de ter determinado que vale a pena a aplicar a otimização de baixo nível, então é um tempo para entender sua plataforma de destino. O custo você pode evitar aqui é a pena de misprediction ramo. No moderno CPU x86 / x64 esta misprediction é provável que seja muito pequena (podem prever chamadas indiretas muito bem na maioria das vezes), mas quando o direcionamento PowerPC ou outras plataformas RISC, as chamadas indiretas / saltos muitas vezes não são previstos em tudo e evitando -los pode causar ganho significativo de performance. Veja também custo da chamada Virtual depende plataforma .

Compiler pode implementar interruptor usando a tabela salto bem

Uma pegadinha: Troca às vezes pode ser implementada como uma chamada indireta (usando uma tabela), bem como, especialmente quando se alterna entre muitos valores possíveis. Tal interruptor apresenta o mesmo misprediction como uma função virtual. Para fazer essa otimização de confiança, um provavelmente preferiria usar se em vez de alternar para o caso mais comum.

Use temporizadores para ver qual é mais rápido. Embora a menos que este código vai ser mais e mais, então é improvável que você vai notar qualquer diferença.

Certifique-se de que se você estiver executando o código a partir do construtor que, se a construção falha que você não vai vazar memória.

Esta técnica é muito usado com Symbian OS: http://www.titu.jyu.fi/modpa/Patterns/ O teste-TwoPhaseConstruction.html

Se você só estão chamando callFoo () uma vez, do que mais provável o ponteiro de função será mais lento por uma quantia insignificante. Se você está chamando-a muitas vezes do que mais provável o ponteiro de função será mais rápido por uma quantia insignificante (porque ele não precisa manter a atravessar o switch).

De qualquer maneira olhada no código montado para descobrir com certeza ele está fazendo o que você pensa que está fazendo.

Um frequentemente esquecido vantagem para switch (mesmo sobre a classificação e indexação) é se você sabe que um determinado valor é usado na grande maioria dos casos. É fácil encomendar o interruptor de modo que o mais comum são verificados primeiro.

ps. Para reforçar a resposta de Greg, se você se preocupa com velocidade - medida. Olhando para assembler não ajuda quando CPUs têm prefetch / preditivo ramificação e gasoduto barracas etc

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top