As regras de programação de Rob Pike (1989)
(users.ece.utexas.edu)- Artigo de 1989 sobre as 5 regras de programação de Rob Pike
- Regra 1: não presuma onde o programa passará a maior parte do tempo; gargalos podem surgir de forma inesperada. Evite hacks de desempenho até que o gargalo seja comprovado.
- Regra 2: sempre meça antes de ajustar em busca de velocidade. Otimize apenas quando uma parte do código tiver impacto significativo sobre o restante.
- Regra 3: algoritmos complexos são lentos quando
né pequeno. Esse é o caso na maioria das vezes. Use algoritmos complexos apenas quandoncostuma ser grande e, mesmo assim, aplique primeiro a Regra 2. - Regra 4: algoritmos e estruturas de dados simples são preferíveis. São menos propensos a bugs e mais fáceis de implementar do que os complexos.
- Regra 5: a estrutura de dados correta é decisiva na programação. Se os dados estiverem bem organizados, o algoritmo se tornará óbvio.
- As regras 1 e 2 de Pike refletem o aforismo de Tony Hoare: "otimização prematura é a raiz de todo mal".
- Ken Thompson reformulou as regras 3 e 4 de Pike como: "na dúvida, use força bruta".
- As regras 3 e 4 implementam a filosofia de design KISS (Keep It Simple, Stupid).
- A Regra 5 está alinhada com uma observação de Fred Brooks em 'The Mythical Man-Month', muitas vezes resumida como "escreva código burro que use objetos inteligentes".
1 comentários
Comentários do Hacker News
Concordo totalmente com a frase “os dados dominam”
Por isso entrevistas do LeetCode sempre me pareceram estranhas. Em geral, elas se concentram em algoritmos, mas, na prática, muitas vezes não se deve abordar o problema assim desde o começo; estruturas de dados deveriam ser mais centrais
Claro que, se você não souber nada de algoritmos, pode não perceber casos de exceção ou momentos em que precisa depender de um algoritmo específico por algum motivo. Ainda assim, algoritmos podem ser ensinados de forma relativamente breve, enquanto ter intuição sobre qual estrutura de dados usar parece ser algo em que as pessoas têm mais dificuldade
Naquele momento, a atmosfera muda para “ah, entrou aqui um engenheiro sênior de verdade”, e a conversa sobre problemas técnicos fica mais aberta, com menos atitude de tentar provar “se sabe programar”
Por outro lado, as equipes em que foi mais difícil promover boas mudanças, cumprir milestones e colaborar eram aquelas em que não havia ninguém que definisse bem as estruturas de dados e a arquitetura de código. Parece que muita gente se acostumou com a ideia de que o framework faz tudo e, se não fizer, algum plugin ou middleware criado por alguém mais inteligente vai resolver
Um engenheiro que evita estruturas de dados está dando um tiro no próprio pé e abrindo mão de uma das ferramentas mais úteis, então suas limitações aparecem no dia a dia
Por exemplo, o código pode ser bastante usado para encontrar o caminho mais longo em um grafo acíclico direcionado (DAG) com pesos, mas o ponto central era perceber que o problema podia ser representado como um DAG ponderado. Se você não enxergar isso, ainda pode resolver, mas a solução fica muito mais lenta e complexa
O entrevistador não vai dizer de antemão que você deve usar uma fila de prioridade, uma matriz de adjacência ou uma trie. Se você travar, ele pode dar uma dica, mas condução excessiva dificilmente parece um sinal forte de contratação
Sobre a frase “algoritmos sofisticados são lentos quando n é pequeno, e n geralmente é pequeno”, o que senti em um projeto recente é que n grande pode ser muito maior do que se imagina
É fácil pensar “preciso fazer 100 mil operações, então tenho que otimizar”, mas computadores são rápidos, e algo como 100 mil multiplicações geralmente é tão rápido que talvez nem valha a pena pensar muito nisso
Não estou dizendo para não pensar em nada, mas é frequentemente surpreendente o quão absurdamente rápido é o hardware moderno
Já vi incidentes em produção causados por código que, por engano, virou quadrático; e, mesmo que 99% dos usuários sempre usem n pequeno, alguns usuários podem se deparar com n grande com frequência e sofrer com um app muito lento
Na maioria dos casos, eu preferiria escolher um algoritmo melhor que quadrático, mesmo que seja um pouco mais lento no caso comum e um pouco mais complexo de implementar. Caminhos lentos comuns são otimizados, mas caminhos lentos raros passam despercebidos porque o desenvolvedor não os percorre diretamente, ou estouram em produção
Claro, se o algoritmo for complexo demais, eu poderia escolher uma implementação simples de tempo quadrático, mas meu padrão é mirar em menos que quadrático sempre que possível. Também escrevi um texto relacionado: https://kevincox.ca/2023/05/09/less-than-quadratic/
Então isso talvez fizesse mais sentido 40 anos atrás, quando a CPU não era tão mais rápida que a memória e não se dava tanta atenção a falhas de previsão de desvio em hardware de consumo
A cada entrevista, acontece de o gerente de contratação querer isso, mas um iniciante em LeetCode que ainda não sofreu as feridas da produção se recusar a tomar a decisão
https://www.frankmcsherry.org/assets/COST.pdf
Considerando o aumento de desempenho dos computadores nos últimos 20 anos, parece bastante razoável elevar esse critério para 100 mil
O famoso aforismo “otimização prematura é a raiz de todo mal” na verdade vem de Donald Knuth, não de Tony Hoare, e costuma ser usado sem contexto como se fosse contra otimização em geral
A frase completa é: “Devemos esquecer pequenas eficiências, digamos em 97% dos casos. Otimização prematura é a raiz de todo mal. Mas não devemos perder as oportunidades naquele 3% crítico”
A ideia é gastar tempo otimizando onde há impacto
Parece possível que Tony tenha dito primeiro e Knuth tenha refinado e publicado. É sempre bom manter junto a citação mais longa, que dá o contexto necessário
Programar naquela época era muito diferente de hoje. A “otimização prematura” de então não era “vamos simplesmente usar uma biblioteca popular e escalável”, mas algo mais próximo de “vamos usar um algoritmo incompreensível de manipulação de bits que só funciona neste hardware”
Esse sentido já está incluído em “otimização prematura é a raiz de todo mal”; o aforismo não é “otimização é a raiz de todo mal”
Em entrevistas de estruturas de dados e algoritmos na empresa, vi incontáveis desenvolvedores front-end dizerem que bubble sort era a melhor opção. Não é preciso derivar tudo na hora; basta conhecer algumas alternativas e indicar uma boa escolha para o problema
Se alguém leva “não faça otimização prematura” a um extremo tão grande que nem conhece métodos eficientes, como vai saber onde está o ponto importante?
A ideia de que “estruturas de dados são o ponto central” é duas vezes mais importante em bancos de dados
Pessoas que usam o DB como um repositório burro de bits ou como mero reflexo 1:1 de definições de objetos costumam se surpreender quando o DB leva isso para o lado pessoal e destrói a performance
Se eu tiver que ver de novo um schema de DB gerado por ORM, vai ser um reencontro cedo demais
O problema é que alguns, ou muitos, desenvolvedores não sabem SQL e tampouco têm o conhecimento de DB necessário para usar um ORM
ORM é uma abstração bastante vazada, na qual é preciso saber o que existe por baixo. Entendendo isso, dá para criar schemas decentes com a maioria dos ORMs
Para organizar bem as estruturas de dados e mantê-las assim mesmo quando o design muda, é preciso separar dados e código no nível organizacional
O projeto do schema do DB, os casos de uso e o mapeamento entre eles devem ser separados do restante da implementação, e esse grupo também deve escrever verificações de integridade etc. Se a estrutura organizacional não separa dados e código, é difícil separar código e dados
Minha regra adicional é que pequenos desperdícios de performance, quando se acumulam, acabam deixando o programa lento, mesmo que individualmente não pareçam grande coisa
Se não há impacto em complexidade, legibilidade, manutenibilidade ou custo de implementação, não se deve simplesmente jogar performance fora. Mantidas as demais condições quase iguais, não é certo escolher a alternativa mais lenta entre duas opções
Além disso, quando se assume que n é pequeno, quase tudo funciona. Mas, se você escreve um código que funciona bem com n abaixo de 100 e quebra acima de 10000, por exemplo algo O(n²), então deveria simplesmente impor um limite. Se a suposição de n pequeno for violada, é melhor falhar ruidosamente do que receber uma conta explosiva da AWS ou ter um programa travado
Boa parte dessas diretrizes acaba convergindo para estratégias de evitar overengineering
Na minha experiência, otimização prematura é uma das armadilhas mais caras. Quando você contorna cedo demais um problema potencial, essa suposição não é validada, e a próxima equipe precisa criar uma solução cara para lidar com uma complexidade desnecessária
A abordagem que aprendi é esta: otimização depende de estimativas, e estimativas no início frequentemente estão erradas
Também aprendi que gestão de ego e entendimento psicológico são bastante importantes para evitar que as pessoas produzam código excessivamente complexo
Superprodução costuma ser considerada o pior desperdício, porque não apenas cria algo de que não se precisa, como também consome esforço que poderia ter sido usado no que era realmente necessário. Overengineering é parecido
Por exemplo, começar com uma arquitetura de microsserviços mesmo tendo apenas 100 usuários, porque talvez um dia sejam 1 milhão e redesenhar o monólito seria difícil
Por isso, é preciso tratar primeiro de por que o código fica menos maleável com o passar do tempo
Em geral são boas regras, mas, na prática, a regra 1 não se sustenta exatamente assim
No início, é preciso ter uma hipótese sobre o que será o gargalo. Nem sempre dá para simplesmente implementar X, Y e Z, depois medir o que está lento e corrigir. X, Y e Z podem estar interligados, de modo que, para tornar Y rápido, seja necessário fazer X e Z de uma determinada forma; e às vezes você já sabe que Y será o gargalo
Mesmo que, medindo depois, você descubra o que está lento, ainda precisa apostar em uma abordagem para torná-lo mais rápido. Quanto mais informada for a aposta, melhor
Bons programadores medem, mas conseguem prever o que será lento, terá muitos bugs e consumirá muita memória, reduzindo o número de iterações. Dizer como regra que não dá para prever o comportamento de desempenho acaba ignorando a experiência e a habilidade acumuladas por bons programadores
Isso porque o próprio processo de seguir a regra 1 é a melhor maneira de obter a experiência e o repertório empírico necessários para desenvolver uma boa intuição sobre gargalos
Se sua previsão de desempenho estiver errada, você pode carregar código desnecessariamente complexo por toda a vida do projeto
As pessoas erram com frequência ao estimar a velocidade de algoritmos. Se o computador passa 99% do tempo buscando n no servidor de banco de dados, muitas vezes O(n) e O(n²) parecem iguais no tempo real
Às vezes um algoritmo escrito em C é mais lento que um código equivalente em Python, possivelmente porque o compilador de bytecode fez algo inteligente
Já trabalhei bastante tornando código legado mais rápido, e geralmente é muito mais fácil do que se imagina; ele é lento por motivos que não eram óbvios para o autor original. Na prática, muitas vezes é lento porque a base de código ficou tão complexa que o autor original já não conseguia mais raciocinar sobre ela. Para mim, há um exemplo concreto de algo “lento demais”, então é fácil executar, observar onde está lento e depurar
Se você está fazendo um videogame com muitos objetos físicos e sabe com certeza, por experiência, que detecção de colisão será um grande problema, projetar o jogo e o sistema em torno disso não é um hack de velocidade
Se você está trabalhando em algo em que desempenho certamente será uma preocupação importante, é claro que deve medir. Não para confirmar se é uma preocupação, mas para verificar quão bem você está lidando com ela
Se você está criando um novo sistema para novos requisitos, muitas vezes parece aceitável simplesmente começar. Construir, testar e medir, jogar fora ou refatorar, e repetir
Pegando Rust como exemplo, ele começou com uma linguagem de rascunho e um compilador feito em OCaml, e foi iterando. Mesmo que se soubesse que um dia ele poderia sair de OCaml para self-hosting, não sei se isso teria feito grande diferença
Se não há usuários, até uma função que leva horas ainda é rápida o suficiente em comparação com uma função que, quando otimizada, passa a levar milissegundos. Não sei se alguém já demonstrou conseguir fazer esse tipo de previsão com precisão
Como contraponto à regra 5, algoritmos complexos sobre dados simples podem trazer grandes ganhos de desempenho, remover obstáculos e até simplificar as coisas
Por exemplo, usar busca binária em um array ordenado em vez de um objeto BinaryTree torna a mesclagem simples — concatena e ordena —, facilita a serialização por não haver ponteiros e, em alguns casos, elimina a própria necessidade de serialização. O array pode estar em disco, em memória, ou em ambos via mmap; pode lidar com dados maiores que a RAM; e também permite cold start apontando apenas para o arquivo ou mapeamento e executando imediatamente. Também tem características cache-oblivious
Codificação de Huffman é outro exemplo. Na universidade, normalmente se aprende o algoritmo baseado em árvore e complexidade O(n log n), mas eu não sabia que havia uma forma de construir uma árvore de Huffman em tempo linear usando um array in-place
Claro, 99% do tempo estamos criando microsserviços de backend e usando estruturas de dados de coleções padrão. Mas, se eu estivesse trabalhando com big data no emprego, preferiria muito mais processar tudo em uma única máquina local com discos grandes do que adotar a família MapReduce da moda na época
Rob Pike provavelmente diria para primeiro perfilar o código e, depois, verificar se o código sofisticado ou a estrutura de dados alternativa é de fato mais rápida
Li este texto pela primeira vez no cat-v há mais de 10 anos, e ele teve um impacto indelével na forma como abordo e penso sobre design e complexidade
http://doc.cat-v.org/bell_labs/pikestyle
Não entendo como a regra original “estruturas de dados são o ponto central” vira “escreva código burro usando objetos inteligentes”
A expressão “smart objects” ficou muito ruim; a regra original, mesmo sendo mais longa, é muito melhor