Por que usar uma abordagem OO em vez de uma declaração gigante "switch"?


59

Estou trabalhando em uma loja .Net, C # e tenho um colega de trabalho que insiste em que devemos usar instruções gigantes do Switch em nosso código com muitos "casos" em vez de abordagens mais orientadas a objetos. Seu argumento sempre volta ao fato de que uma instrução Switch é compilada em uma "tabela de salto da CPU" e, portanto, é a opção mais rápida (embora em outras coisas nossa equipe seja informada de que não nos importamos com velocidade).

Sinceramente, não tenho argumento contra isso ... porque não sei do que diabos ele está falando.
Ele está certo?
Ele está apenas falando mal?
Apenas tentando aprender aqui.


7
Você pode verificar se ele está certo, usando algo como o .NET Reflector para verificar o código do assembly e procurar a "tabela de salto da CPU".
FrustratedWithFormsDesigner

5
"A instrução Switch alterna para uma" tabela de salto da CPU "O mesmo acontece com o método de pior caso, com todas as funções virtuais puras. Nenhuma função virtual é simplesmente vinculada diretamente. Você despejou algum código para comparar?
S.Lott

64
O código deve ser escrito para PEOPLE, não para máquinas, caso contrário, faríamos tudo na montagem.
maple_shaft

8
Se ele é tão barulhento, cite Knuth para ele: "Devemos esquecer pequenas eficiências, digamos 97% das vezes: a otimização prematura é a raiz de todo mal".
DaveE 25/05

12
Manutenção. Quaisquer outras perguntas com respostas de uma palavra que eu possa ajudá-lo?
Matt Ellen

Respostas:


48

Ele provavelmente é um velho hacker C e, sim, ele está falando mal. .Net não é C ++; o compilador .Net continua melhorando e os hacks mais inteligentes são contraproducentes, se não hoje, então na próxima versão .Net. Pequenas funções são preferíveis porque .Net JIT-s todas as funções uma vez antes de serem usadas. Portanto, se alguns casos nunca forem atingidos durante o Ciclo de Vida de um programa, não haverá custos na compilação do JIT. De qualquer forma, se a velocidade não é um problema, não deve haver otimizações. Escreva primeiro para o programador, para o segundo do compilador. Seu colega de trabalho não será facilmente convencido, então eu provaria empiricamente que um código melhor organizado é realmente mais rápido. Eu escolhia um dos piores exemplos dele, reescrevia-os de uma maneira melhor e depois me certificava de que seu código fosse mais rápido. Escolha uma cereja, se necessário. Em seguida, execute-o alguns milhões de vezes, analise e mostre a ele.

EDITAR

Bill Wagner escreveu:

Item 11: Entenda a atração de pequenas funções (C # Segunda edição efetiva) Lembre-se de que traduzir seu código C # em código executável por máquina é um processo de duas etapas. O compilador C # gera IL que é entregue em assemblies. O compilador JIT gera código de máquina para cada método (ou grupo de métodos, quando o embutimento está envolvido), conforme necessário. Pequenas funções tornam muito mais fácil para o compilador JIT amortizar esse custo. Também é provável que pequenas funções sejam candidatas a inlining. Não é apenas a pequenez: o fluxo de controle mais simples é importante também. Menos ramificações de controle dentro das funções facilitam o registro do compilador JIT nas variáveis. Não é apenas uma boa prática escrever código mais claro; é assim que você cria código mais eficiente em tempo de execução.

EDIT2:

Então ... aparentemente uma instrução switch é mais rápida e melhor do que várias instruções if / else, porque uma comparação é logarítmica e outra é linear. http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

Bem, minha abordagem favorita para substituir uma declaração de chave grande é com um dicionário (ou algumas vezes até uma matriz, se eu estiver ativando enums ou pequenas ints) que está mapeando valores para funções chamadas em resposta a elas. Fazer isso obriga a remover muitos estados desagradáveis ​​de espaguete compartilhado, mas isso é uma coisa boa. Uma declaração de chave grande geralmente é um pesadelo de manutenção. Portanto ... com matrizes e dicionários, a pesquisa levará um tempo constante e haverá pouca memória extra desperdiçada.

Ainda não estou convencido de que a declaração do switch seja melhor.


47
Não se preocupe em provar isso mais rápido. Isso é otimização prematura. O milissegundo que você pode economizar não é nada comparado ao índice que você esqueceu de adicionar ao banco de dados que custa 200ms. Você está travando a batalha errada.
Rein Henrichs

27
@ Job e se ele estiver realmente certo? O ponto não é que ele esteja errado, o ponto é que ele está certo e isso não importa .
Rein Henrichs

2
Mesmo que ele estivesse certo em cerca de 100% dos casos, ainda está desperdiçando nosso tempo.
Jeremy

6
Quero arrancar meus olhos tentando ler a página que você vinculou.
AttackingHobo

3
O que há com o ódio em C ++? Os compiladores C ++ também estão melhorando, e os grandes comutadores são tão ruins em C ++ quanto em C # e pelo mesmo motivo. Se você está cercado por ex-programadores de C ++ que sofrem com isso, não é porque eles são programadores de C ++, é porque são maus programadores.
Sebastian Redl

39

A menos que seu colega possa fornecer prova de que essa alteração oferece um benefício real mensurável na escala de todo o aplicativo, ela é inferior à sua abordagem (ou seja, polimorfismo), que realmente oferece esse benefício: manutenção.

A microoptimização deve ser feita apenas após a identificação de gargalos. A otimização prematura é a raiz de todo mal .

A velocidade é quantificável. Há pouca informação útil na "abordagem A é mais rápida que na abordagem B". A pergunta é " Quanto mais rápido? ".


2
Absolutamente verdadeiro. Nunca afirme que algo é mais rápido, sempre meça. E meça apenas quando essa parte do aplicativo for o gargalo de desempenho.
Kilian Foth

6
-1 para "Otimização prematura é a raiz de todo mal." Por favor, mostre a citação completa , não apenas uma parte que inclua a opinião de Knuth.
alternativa

2
@mathepic: intencionalmente não apresentei isso como uma citação. Esta frase, como é, é minha opinião pessoal, embora, é claro, não seja minha criação. Embora possa ser notado que os caras do c2 parecem considerar exatamente essa parte a sabedoria central.
Back2dos 25/05

8
@alternative A citação completa de Knuth "Não há dúvida de que o grau de eficiência leva ao abuso. Os programadores perdem muito tempo pensando ou se preocupando com a velocidade das partes não críticas de seus programas, e essas tentativas de eficiência têm realmente um forte impacto negativo ao considerar a depuração e a manutenção. Devemos esquecer as pequenas eficiências, digamos cerca de 97% das vezes: a otimização prematura é a raiz de todo mal. " Descreve o colega de trabalho do OP perfeitamente. Back2dos IMHO resumiu a citação bem com "otimização prematura é a raiz de todo o mal"
MarkJ

2
@MarkJ 97% do tempo
alternativa

27

Quem se importa se é mais rápido?

A menos que você esteja escrevendo software em tempo real, é improvável que a quantidade minúscula de aceleração que você possa obter ao fazer algo de uma maneira completamente insana faça muita diferença para o seu cliente. Eu nem sequer lutaria contra este na frente da velocidade, esse cara claramente não vai ouvir nenhuma discussão sobre o assunto.

A manutenção, no entanto, é o objetivo do jogo, e uma declaração de troca gigante não é nem um pouco sustentável, como você explica os diferentes caminhos do código para um novo pessoal? A documentação terá que ser tão longa quanto o próprio código!

Além disso, você tem a total incapacidade de testar a unidade de maneira eficaz (muitos caminhos possíveis, sem mencionar a provável falta de interfaces etc.), o que torna seu código ainda menos sustentável.

[Do lado do interesse: o JITter tem um desempenho melhor em métodos menores, para que instruções de comutação gigantescas (e seus métodos inerentemente grandes) prejudiquem sua velocidade em grandes montagens, IIRC.]


11
+ um grande exemplo de otimização prematura.
ShaneC 25/05

Definitivamente isso.
DeadMG 25/05

+1 para 'uma instrução switch gigante não é ainda pouco sustentável'
Korey Hinton

2
Uma declaração de troca gigante é muito mais fácil para o novato compreender: todos os comportamentos possíveis são coletados ali mesmo em uma boa lista. As chamadas indiretas são extremamente difíceis de serem seguidas; no pior dos casos (ponteiro de função), é necessário pesquisar em toda a base de códigos as funções da assinatura correta, e as chamadas virtuais são apenas um pouco melhores (procure funções com o nome e a assinatura certos e relacionados por herança). Mas a manutenção não se resume a ser somente leitura.
Ben Voigt

14

Afaste-se da instrução switch ...

Esse tipo de declaração de troca deve ser evitado como uma praga, porque viola o Princípio Aberto Fechado . Isso força a equipe a fazer alterações no código existente quando uma nova funcionalidade precisa ser adicionada, em vez de apenas adicionar um novo código.


11
Isso vem com uma ressalva. Existem operações (funções / métodos) e tipos. Quando você adiciona uma nova operação, é necessário alterar o código em apenas um lugar para as instruções switch (adicionar uma nova função com a instrução switch), mas você deve adicionar esse método a todas as classes no caso OO (viola a abertura / princípio fechado). Se você adicionar novos tipos, precisará tocar em cada instrução de chave, mas no caso do OO, basta adicionar mais uma classe. Portanto, para tomar uma decisão informada, você precisa saber se adicionará mais operações aos tipos existentes ou mais tipos.
Scott Whitlock

3
Se você precisar adicionar mais operações aos tipos existentes em um paradigma OO sem violar o OCP, acredito que é para isso que serve o padrão de visitante.
Scott Whitlock

3
@ Martin - chame o nome se quiser, mas essa é uma troca bem conhecida. Remeto-o ao Código Limpo, por RC Martin. Ele revisita seu artigo sobre OCP, explicando o que descrevi acima. Você não pode projetar simultaneamente para todos os requisitos futuros. Você precisa escolher se é mais provável adicionar mais operações ou mais tipos. OO favorece a adição de tipos. Você pode usar o OO para adicionar mais operações se modelar operações como classes, mas isso parece entrar no padrão de visitantes, que possui seus próprios problemas (principalmente custos indiretos).
Scott Whitlock

8
@ Martin: Você já escreveu um analisador? É bastante comum ter casos de comutação grandes que ativam o próximo token no buffer lookahead. Você pode substituir esses comutadores por chamadas de função virtual para o próximo token, mas isso seria um pesadelo de manutenção. É raro, mas às vezes a caixa de opções é realmente a melhor escolha, pois mantém o código que deve ser lido / modificado juntos nas proximidades.
Nikie 25/05

11
@ Martin: Você usou palavras como "nunca", "sempre" e "Poppycock", então eu estava assumindo que você está falando de todos os casos sem exceções, não apenas do caso mais comum. (E BTW: As pessoas ainda escrever analisadores à mão Por exemplo, o analisador CPython ainda é escrito à mão, IIRC..)
Nikie

8

Eu sobrevivi ao pesadelo conhecido como a enorme máquina de estados finitos manipulada por enormes comandos switch. Pior ainda, no meu caso, o FSM abrangeu três DLLs C ++ e ficou bem claro que o código foi escrito por alguém versado em C.

As métricas com as quais você precisa se preocupar são:

  • Velocidade de fazer uma mudança
  • Velocidade de encontrar o problema quando isso acontece

Foi-me dada a tarefa de adicionar um novo recurso a esse conjunto de DLLs e consegui convencer o gerenciamento de que levaria tanto tempo para reescrever as 3 DLLs quanto uma DLL orientada a objeto corretamente, como seria para mim o patch do macaco e o júri coloca a solução no que já estava lá. A reescrita foi um enorme sucesso, pois não apenas suportava a nova funcionalidade, mas era muito mais fácil de estender. De fato, uma tarefa que normalmente levaria uma semana para garantir que você não quebrasse nada levaria algumas horas.

E quanto aos tempos de execução? Não houve aumento ou diminuição de velocidade. Para ser justo, nosso desempenho foi prejudicado pelos drivers do sistema; portanto, se a solução orientada a objetos fosse de fato mais lenta, não saberíamos disso.

O que há de errado com instruções de comutação massivas para uma linguagem OO?

  • O fluxo de controle do programa é retirado do objeto a que pertence e colocado fora do objeto
  • Muitos pontos de controle externo se traduzem em muitos lugares que você precisa revisar
  • Não está claro onde o estado é armazenado, principalmente se o comutador estiver dentro de um loop
  • A comparação mais rápida é sem comparação (você pode evitar a necessidade de muitas comparações com um bom design orientado a objetos)
  • É mais eficiente percorrer seus objetos e sempre chamar o mesmo método em todos os objetos do que alterar seu código com base no tipo de objeto ou enumeração que codifica o tipo.

8

Eu não compro o argumento de desempenho; é tudo sobre manutenção de código.

MAS: às vezes , é mais fácil manter uma instrução de switch gigante (menos código) do que várias classes pequenas substituindo funções virtuais de uma classe base abstrata. Por exemplo, se você implementasse um emulador de CPU, não implementaria a funcionalidade de cada instrução em uma classe separada - basta inseri-lo em um swtich gigante no código de operação, possivelmente chamando funções auxiliares para obter instruções mais complexas.

Regra geral: se o switch for de alguma forma executado no TYPE, você provavelmente deve usar herança e funções virtuais. Se a chave for executada em um VALUE de um tipo fixo (por exemplo, o código de operação da instrução, como acima), não há problema em deixá-la como está.


5

Você não pode me convencer de que:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

É significativamente mais rápido que:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

Além disso, a versão OO é apenas mais sustentável.


8
Para algumas coisas e para quantidades menores de ações, a versão OO é muito mais divertida. Ele precisa ter algum tipo de fábrica para converter algum valor na criação de uma IAction. Em muitos casos, é muito mais legível apenas ativar esse valor.
Zan Lynx

@Zan Lynx: Seu argumento é muito genérico. A criação do objeto IAction é tão difícil quanto recuperar o número inteiro da ação nem mais difícil nem mais fácil. Para que possamos ter uma conversa real sem ser genérico. Considere uma calculadora. Qual é a diferença de complexidade aqui? A resposta é zero. Como todas as ações pré-criadas. Você recebe a entrada do usuário e já é uma ação.
Martin York

3
@ Martin: Você está assumindo um aplicativo de calculadora GUI. Vamos usar um aplicativo de calculadora de teclado escrito para C ++ em um sistema incorporado. Agora você tem um número inteiro de código de verificação a partir de um registro de hardware. Agora, o que é menos complexo?
Zan Lynx

2
@ Martin: Você não vê como inteiro -> tabela de pesquisa -> criação de novo objeto -> chamar função virtual é mais complicado do que inteiro -> switch -> função? Como você não vê isso?
Zan Lynx

2
@ Martin: Talvez eu vou. Enquanto isso, explique como você faz o objeto IAction chamar action () de um número inteiro sem uma tabela de pesquisa.
Zan Lynx

4

Ele está certo de que o código de máquina resultante provavelmente será mais eficiente. O compilador essencial transforma uma instrução switch em um conjunto de testes e ramificações, que serão relativamente poucas instruções. Há uma grande chance de que o código resultante de abordagens mais abstratas exija mais instruções.

NO ENTANTO : É quase certo que seu aplicativo em particular não precisa se preocupar com esse tipo de micro-otimização, ou você não usaria .net em primeiro lugar. Para qualquer coisa que não seja aplicativos embutidos muito restritos ou trabalho intensivo da CPU, você sempre deve deixar o compilador lidar com a otimização. Concentre-se em escrever código limpo e sustentável. Isso quase sempre tem um valor muito grande do que alguns décimos de um nanossegundo em tempo de execução.


3

Um dos principais motivos para usar classes em vez de instruções switch é que as instruções switch tendem a levar a um arquivo enorme com muita lógica. Isso é um pesadelo de manutenção e também um problema com o gerenciamento de fontes, pois é necessário verificar e editar esse arquivo enorme em vez de arquivos de classe menores diferentes


3

uma instrução switch no código OOP é uma forte indicação de classes ausentes

tente nos dois sentidos e execute alguns testes simples de velocidade; as chances são de que a diferença não é significativa. Se eles são e o código é de tempo crítico , mantenha a instrução switch


3

Normalmente eu odeio a palavra "otimização prematura", mas isso cheira a isso. Vale a pena notar que Knuth usou essa famosa citação no contexto de pressionar para usar gotoinstruções para acelerar o código em áreas críticas . Essa é a chave: caminhos críticos .

Ele estava sugerindo o uso gotopara acelerar o código, mas alertando contra os programadores que desejam fazer esse tipo de coisa com base em palpites e superstições de códigos que nem são críticos.

Favorecer switchdeclarações o máximo possível de maneira uniforme em toda a base de código (independentemente de qualquer carga pesada ser manipulada) é o exemplo clássico do que Knuth chama de programador "tostão e tolo" que passa o dia todo lutando para manter seus "otimizados" "código que se transformou em um pesadelo de depuração como resultado da tentativa de economizar centavos por libras. Esse código raramente pode ser mantido e muito menos eficiente em primeiro lugar.

Ele está certo?

Ele está correto da perspectiva da eficiência muito básica. Nenhum compilador que eu saiba pode otimizar código polimórfico envolvendo objetos e envio dinâmico melhor do que uma instrução switch. Você nunca terminará com uma tabela LUT ou de salto para código embutido a partir de código polimórfico, pois esse código tende a servir como uma barreira otimizadora para o compilador (ele não saberá qual função chamar até o momento em que o envio dinâmico ocorre).

É mais útil não pensar nesse custo em termos de tabelas de salto, mas mais em termos da barreira de otimização. Para o polimorfismo, a chamada Base.method()não permite que o compilador saiba qual função realmente será chamada se methodfor virtual, não selada e puder ser substituída. Como não sabe para qual função realmente será chamada antecipadamente, não pode otimizar a chamada de função e utilizar mais informações para tomar decisões de otimização, pois na verdade não sabe para qual função será chamada. a hora em que o código está sendo compilado.

Os otimizadores estão no seu melhor quando podem espiar uma chamada de função e fazer otimizações que achatam completamente o chamador e o receptor, ou pelo menos otimizam o chamador para trabalhar com mais eficiência com o receptor. Eles não podem fazer isso se não souberem qual função será realmente chamada com antecedência.

Ele está apenas falando mal?

Usar esse custo, que geralmente equivale a centavos, para justificar transformar isso em um padrão de codificação aplicado de maneira uniforme é geralmente muito tolo, especialmente para locais com necessidade de extensibilidade. Essa é a principal coisa que você deseja observar com otimizadores prematuros genuínos: eles querem transformar pequenas preocupações de desempenho em padrões de codificação aplicados uniformemente em toda uma base de código, sem considerar a manutenção.

Eu me ofereço um pouco com a citação "old C hacker" usada na resposta aceita, já que sou uma dessas. Nem todo mundo que codifica há décadas a partir de hardware muito limitado se transformou em um otimizador prematuro. No entanto, eu também encontrei e trabalhei com eles. Mas esses tipos nunca medem coisas como erros de previsão de ramificação ou erros de cache, eles acham que sabem melhor e baseiam suas noções de ineficiência em uma complexa base de código de produção baseada em superstições que não são verdadeiras hoje em dia e às vezes nunca são verdadeiras. As pessoas que realmente trabalharam em campos críticos para o desempenho geralmente entendem que a otimização efetiva é uma priorização efetiva, e tentar generalizar um padrão de codificação degradante da manutenção para economizar centavos é uma priorização muito ineficaz.

Moedas de um centavo são importantes quando você tem uma função barata que não faz tanto trabalho, que é chamado um bilhão de vezes em um loop muito apertado e crítico para o desempenho. Nesse caso, acabamos economizando 10 milhões de dólares. Não vale a pena raspar centavos quando você tem uma função chamada duas vezes, pela qual o corpo por si só custa milhares de dólares. Não é aconselhável gastar seu tempo discutindo moedas de um centavo durante a compra de um carro. Vale a pena pechinchar se você estiver comprando um milhão de latas de refrigerante de um fabricante. A chave para uma otimização eficaz é entender esses custos em seu contexto adequado. Alguém que tenta economizar centavos em cada compra e sugere que todo mundo tente pechinchar, não importa o que está comprando, não é um otimizador qualificado.


2

Parece que seu colega de trabalho está muito preocupado com o desempenho. Pode ser que, em alguns casos, uma estrutura grande de caixa / comutador tenha um desempenho mais rápido, mas espero que vocês façam um experimento realizando testes de temporização na versão OO e na versão comutadora / caixa. Acho que a versão OO tem menos código e é mais fácil de seguir, entender e manter. Eu argumentaria pela versão OO primeiro (como a manutenção / legibilidade deve ser inicialmente mais importante), e só considerarei a versão switch / case somente se a versão OO tiver sérios problemas de desempenho e for possível mostrar que um switch / case fará um melhoria significativa.


11
Juntamente com os testes de temporização, um despejo de código pode ajudar a mostrar como funciona o envio do método C ++ (e C #).
S.Lott

2

Uma vantagem de manutenção do polimorfismo mencionada por ninguém é que você poderá estruturar seu código de maneira muito mais agradável usando a herança se você estiver sempre alternando na mesma lista de casos, mas algumas vezes vários casos são tratados da mesma maneira e às vezes eles não são

Por exemplo. se você estiver alternando entre Dog, Cate Elephant, e às vezes Doge Cattiver o mesmo caso, poderá fazê-los herdar de uma classe abstrata DomesticAnimale colocar essas funções na classe abstrata.

Além disso, fiquei surpreso que várias pessoas usaram um analisador como um exemplo de onde você não usaria polimorfismo. Para um analisador parecido com uma árvore, essa é definitivamente a abordagem errada, mas se você tiver algo como montagem, em que cada linha é um pouco independente e comece com um código de operação que indica como o restante da linha deve ser interpretado, eu usaria totalmente o polimorfismo e uma fábrica. Cada classe pode implementar funções como ExtractConstantsou ExtractSymbols. Eu usei essa abordagem para um intérprete de brinquedo BASIC.


Um switch também pode herdar comportamentos, através do seu caso padrão. "... estende BaseOperationVisitor" torna-se "padrão: BaseOperation (nó)"
Samuel Danielson

0

"Devemos esquecer pequenas eficiências, digamos, 97% das vezes: a otimização prematura é a raiz de todo mal"

Donald Knuth


0

Mesmo que isso não tenha prejudicado a capacidade de manutenção, não acredito que seja melhor para o desempenho. Uma chamada de função virtual é simplesmente um indireção extra (o mesmo que o melhor caso para uma instrução switch), portanto, mesmo em C ++, o desempenho deve ser aproximadamente igual. No C #, onde todas as chamadas de função são virtuais, a instrução switch deve ser pior, pois você tem a mesma sobrecarga de chamada de função virtual nas duas versões.


11
Faltando "não"? Em C #, nem todas as chamadas de função são virtuais. C # não é Java.
Ben Voigt

0

Seu colega não está falando mal por trás, no que diz respeito ao comentário sobre as tabelas de saltos. No entanto, usar isso para justificar a escrita de códigos incorretos é o que ele dá errado.

O compilador C # converte instruções de opção com apenas alguns casos em uma série de if / else, portanto, não é mais rápido do que usar if / else. O compilador converte instruções de opção maiores em um dicionário (a tabela de salto à qual seu colega se refere). Consulte esta resposta para uma pergunta de estouro de pilha no tópico para obter mais detalhes .

É difícil ler e manter uma declaração de chave grande. Um dicionário de "casos" e funções é muito mais fácil de ler. Como é assim que o switch se transforma, você e seu colega devem usar dicionários diretamente.


0

Ele não está necessariamente falando maluco. Pelo menos nas switchinstruções C e C ++ pode ser otimizado para pular tabelas enquanto eu nunca vi isso acontecer com um despacho dinâmico em uma função que só tem acesso a um ponteiro base. No mínimo, o último requer um otimizador muito mais inteligente, olhando para um código muito mais próximo para descobrir exatamente qual subtipo está sendo usado em uma chamada de função virtual por meio de um ponteiro / referência base.

Além disso, o despacho dinâmico geralmente serve como uma "barreira de otimização", o que significa que o compilador geralmente não será capaz de incorporar código e alocar registros de maneira ideal para minimizar derramamentos de pilha e todo esse material sofisticado, pois não consegue descobrir o que a função virtual será chamada pelo ponteiro base para incorporá-la e fazer toda a sua mágica de otimização. Não sei se você deseja que o otimizador seja tão inteligente e tente otimizar chamadas de função indiretas, pois isso pode levar a que muitos ramos de código sejam gerados separadamente em uma pilha de chamadas (uma função que as chamadas foo->f()teriam gerar código de máquina totalmente diferente daquele que chamabar->f() através de um ponteiro de base, e a função que chama essa função teria que gerar duas ou mais versões de código e assim por diante - a quantidade de código de máquina sendo gerada seria explosiva - talvez não tão ruim com um rastreamento JIT que gera o código em tempo real enquanto rastreia os caminhos de execução quentes).

No entanto, como muitas respostas ecoaram, essa é uma péssima razão para favorecer um monte de switchdeclarações, mesmo que sejam de mãos dadas mais rapidamente em alguma quantia marginal. Além disso, quando se trata de microeficiências, coisas como ramificação e inlining geralmente são de baixa prioridade em comparação com coisas como padrões de acesso à memória.

Dito isto, entrei aqui com uma resposta incomum. Quero defender a manutenção de switchdeclarações sobre uma solução polimórfica quando, e somente quando, você tiver certeza de que haverá apenas um lugar que precisa ser executado switch.

Um exemplo principal é um manipulador de eventos central. Nesse caso, você geralmente não tem muitos locais para lidar com eventos, apenas um (por que é "central"). Nesses casos, você não se beneficia da extensibilidade oferecida por uma solução polimórfica. Uma solução polimórfica é benéfica quando existem muitos lugares que fazem a switchdeclaração analógica . Se você tem certeza de que só haverá uma, uma switchinstrução com 15 casos pode ser muito mais simples do que projetar uma classe base herdada por 15 subtipos com funções substituídas e uma fábrica para instancia-las, apenas para ser usada em uma função em todo o sistema. Nesses casos, adicionar um novo subtipo é muito mais entediante do que adicionar uma caseinstrução a uma função. Eu argumentaria pela manutenção, não pelo desempenho,switch declarações neste caso peculiar em que você não se beneficia da extensibilidade.

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.