Mover o `if` para cima e o `for` para baixo
(matklad.github.io)- Mover as instruções
ifpara o ponto de chamada ajuda a reduzir a complexidade do código - Concentrar a verificação de condições e o tratamento de ramificações em um só lugar facilita identificar duplicações e checagens desnecessárias
- Usar a refatoração de dissolução de enum ajuda a evitar que a mesma condição se espalhe por várias partes do código
- Laços
forbaseados em operações em lote são eficazes para melhorar o desempenho e otimizar tarefas repetitivas - Combinar o padrão de subir o
ife descer oforpode aumentar ao mesmo tempo a legibilidade e a eficiência do código
Uma breve nota sobre duas regras relacionadas
- Quando há uma condição
ifdentro de uma função, recomenda-se pensar se ela pode ser movida para o ponto onde a função é chamada - Como no exemplo, em vez de verificar a precondition (pré-condição) dentro da função, o ideal é deixar essa verificação para o chamador ou garantir a pré-condição por meio do tipo (ou de
assert) - A abordagem de elevar (push up) as verificações de pré-condição afeta o código como um todo e, no resultado geral, reduz a quantidade de verificações de condição desnecessárias
Concentração do fluxo de controle e das condições
- O fluxo de controle e as instruções
ifsão grandes fontes de complexidade e bugs no código - É útil concentrar as condições em níveis mais altos, como no ponto de chamada, deixando a lógica de ramificação reunida em uma função e delegando o trabalho real a sub-rotinas lineares (straight-line)
- Quando as ramificações e o fluxo de controle ficam reunidos em um só lugar, fica mais fácil identificar ramificações duplicadas e condições desnecessárias
Exemplos:
- Quando há
ifs aninhados dentro da função f, fica mais fácil reconhecer código morto (Dead Branch) - Quando as ramificações ficam distribuídas por várias funções (g, h), essa análise se torna mais difícil
Refatoração de dissolução de enum (Dissolving enum Refactor)
- Quando o código embute a mesma lógica de ramificação em um
enumou algo semelhante, é possível puxar a condição para cima e separar com mais clareza as ramificações e o trabalho executado - Ao aplicar essa abordagem, evita-se que a mesma condição apareça repetidas vezes no código
Exemplo:
- Em uma situação em que a mesma condição de ramificação está expressa nas funções f, g e também no
enumE, - é possível simplificar todo o código com uma única ramificação condicional em nível superior
Pensamento orientado a dados (Data Oriented Thinking) e operações em lote
- A maioria dos programas funciona com vários objetos (entidades). No caminho crítico (Hot Path), o desempenho costuma ser determinado pelo processamento de muitos objetos
- É recomendável introduzir o conceito de lote (batch), tomando operações sobre conjuntos de objetos como padrão e tratando operações sobre um único objeto como caso especial
Exemplo:
-
Tendo algo como
frobnicate_batch(walruses)como função de processamento em lote padrão, -
o processamento de objetos individuais pode ser tratado como um caso especial por meio de um laço
for -
Essa abordagem tem papel importante na otimização de desempenho; em cargas grandes, reduz o custo inicial e aumenta a flexibilidade de ordenação
-
Também permite aproveitar operações SIMD (como struct-of-array), processando certos campos em lote antes de prosseguir com o trabalho completo
Casos práticos e padrão recomendado
- Assim como na multiplicação de polinômios baseada em FFT, é possível maximizar o desempenho ao permitir operações simultâneas em vários pontos
- As regras de mover condições para cima e laços para baixo podem ser aplicadas em conjunto
Exemplo:
- Em vez de verificar a mesma condição repetidamente dentro de um laço, ao mover a condição para fora dele reduz-se a quantidade de ramificações no loop e fica mais fácil otimizar e vetorizar
- Essa abordagem garante alta eficiência também em planos de dados de sistemas de grande porte, como no design do TigerBeetle
Conclusão
- Ao combinar o padrão de mover o
if(condição) para a camada superior (chamador, controle) e ofor(repetição) para a camada inferior (operação, processamento de dados), é possível melhorar ao mesmo tempo a legibilidade, a eficiência e o desempenho do código - Pensar do ponto de vista de espaços vetoriais abstratos e de operações sobre conjuntos oferece uma ferramenta melhor para resolver problemas do que ramificações repetitivas
- Em resumo:
ifpara cima,forpara baixo!
1 comentários
Comentários do Hacker News
ifestá dentro da função, considere movê-la para o chamador” tem contraexemplos demais. Se uma função é chamada em 37 lugares, vou repetir o mesmoifem todos eles? Dá para dizer que funções comogetaddrinfoouEnterCriticalSectiondeveriam mover esseifpara fora? Acho que esse tipo de transformação só faz sentido quando há umas duas chamadas no máximo e quando essa decisão está fora das responsabilidades da função. Uma abordagem é delegar para uma função auxiliar que faz só a parte da condicional. E, quando for preciso mover a condição para fora do loop, você pode deixar o chamador usar diretamente um helper de condição mais baixo nível. Mas o centro dessa discussão é “otimização”. E otimização muitas vezes entra em conflito com um projeto de programa melhor. Pode ser um projeto melhor quando o chamador não precisa saber da condição. Esse dilema também aparece bastante em OOP. A decisão representada por um “if” na verdade pode acontecer por despacho de método. Tirar esse despacho de dentro do loop também pode entrar em atrito com princípios de design. Um exemplo é desenhar uma imagem em um canvas usando um método comoblit, em vez de chamarputpixelrepetidamenteEnterCriticalSectiondevem mesmo fazer validações fortes na entrada, inclusive com condicionais. Já em código de aplicação, pode ser perfeitamente aceitável mover oifpara o chamador. Em bibliotecas ou código central, faz sentido empurrar o fluxo de controle para as bordas. Dentro do domínio que você controla, é bom manter o fluxo de controle nas extremidades. Mas esse tipo de regra é sempre idiomática, então alguém capaz de julgar o contexto precisa aplicar isso com bom sensomatchpor chamadas de método polimórficas. O objetivo dessa abordagem é separar o momento em que a decisão inicial da ramificação é tomada do momento em que o comportamento de fato é executado. A distinção entre casos fica carregada pelo objeto, aqui o valor do enum, ou por um closure, então não é preciso repeti-la a cada chamada. Se a distinção entre casos mudar, basta alterar o ponto de bifurcação e não os lugares onde o comportamento acontece. A desvantagem é o trade-off entre a conveniência de enxergar diretamente a ramificação de comportamento de cada caso e o fato de surgir uma dependência, no nível do código, da lista de casosifpara baixo. Mas este texto recomenda o oposto: levar osifpara cima, para funções mais de alto nível. Assim dá para tratar a lógica de decisão complexa de forma centralizada em uma função, enquanto o trabalho concreto de fato é delegado a sub-rotinasif (weShouldDoThis()) { doThis(); }. Se você extrair cada verificação para uma função separada, fica mais fácil testar e gerenciar a complexidadesonarqubedisparam um monte de alertas de “code smell” que não correspondem a bugs reais. Ao tentar corrigir esse tipo de “código que não tem problema”, o risco de introduzir bugs novos aumenta, e você ainda perde tempo que poderia usar para tratar problemas de verdadeifdentro doforpode ser mais fácil de manter e ter acesso à memória mais eficiente do que umiffora dofor. Um exemplo concreto: se a operação for somar 1 aos ímpares e subtrair 2 dos pares, no código original cada iteração do loop precisaria ramificar, mas com SIMD dá para processar 16intde uma vez, sem branch nenhuma. Se o compilador fizer a vetorização corretamente, ele pode transformar o código original nessa versão otimizada sem branchifdentro doforé uma ramificação dependente dos dados, então não dá para puxá-lo facilmente para cima. Se o algoritmo tivesse uma condição fora do loop, algo comoif (length % 2 == 1) { ... } else { ... }, aí sim mover essa condição para cima doforseria obviamente o certo. Na versão com SIMD, oifdesaparece completamente, e esse parece o tipo ideal de padrão de código que o autor do texto também gostaria de verfor. Alguém sabe o quão difícil é para um compilador vetorizar esse tipo de código automaticamente? Tenho curiosidade sobre onde está esse limiteforcriam a abstração de que “oforé a regra e a ramificação é o comportamento”. Só que, quando surgem novos requisitos, essa abstração quebra, e você começa a enfiar parâmetros ou exceções até o código ficar difícil de entender. Se o código tivesse sido escrito sem essa abstração desde o início, o resultado poderia ter ficado mais claro e mais fácil de manter. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction