2 pontos por GN⁺ 2023-11-02 | 1 comentários | Compartilhar no WhatsApp
  • 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 quando n costuma 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

 
GN⁺ 2023-11-02
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

    • Pela minha experiência, concordo. Em entrevistas, quando se passa de uma verificação de algoritmo tipo FizzBuzz diretamente para conversar sobre estruturas de dados, arquitetura e mapeamento para o domínio, vi entrevistadores demonstrarem muito mais respeito
      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
    • Ajudando meu sobrinho a se preparar para competições de programação, vi que, na maioria dos problemas, transformar os dados em uma estrutura de dados adequada era uma grande parte da solução
      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
    • Problemas comuns do LeetCode, na verdade, também se concentram em estruturas de dados. Isso porque o candidato precisa ter na cabeça uma lista de estruturas de dados a acionar ao fazer pattern matching entre o problema e a solução
      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
    • “Mostre-me os fluxogramas e esconda as tabelas, e continuarei confuso. Mostre-me as tabelas, e normalmente não precisarei dos fluxogramas; eles serão óbvios.”
    • Se for para escolher uma das duas coisas, acho que você precisa ter uma noção aproximada da outra. Se não souber nada sobre como acessar os dados, fica difícil saber qual estrutura de dados deve usar
  • 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

    • Tenho dificuldade em concordar fortemente com isso. Algoritmos de tempo quadrático são do tipo que pode morder você em momentos inesperados
      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/
    • A hierarquia de memória também influencia isso. Muitos algoritmos sofisticados têm localidade de referência ruim e adicionam desvios
      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
    • Em problemas de entrevista do LeetCode, vejo o tempo todo listas de 100 mil itens sendo percorridas algumas vezes. Pode não ser ótimo, mas, em termos de tempo real de produção, percorrer 100 mil itens não é nada comparado à chamada de rede que vem logo depois
      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
    • A referência clássica sobre esse tema é Scalability! But at what COST?
      https://www.frankmcsherry.org/assets/COST.pdf
    • Quando comecei meu primeiro emprego de programação de verdade em uma empresa de games no começo dos anos 2000, o diretor técnico me aconselhou: “se a quantidade de itens com que você está lidando for por volta de 10 mil, não otimize”
      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

    • Knuth diz que a frase é de Hoare, e Hoare diz que é de Knuth, então é uma questão de em quem acreditar. Talvez seja melhor atribuí-la aos dois
      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
    • Também se esquece com frequência que essa citação é do fim dos anos 1970. Quase 50 anos atrás
      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”
    • Não vejo a citação longa acrescentando um contexto realmente significativo. Se você mediu e encontrou os 3% importantes, isso já não é mais prematuro
      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”
    • Muita gente aceita essa frase como doutrina e acaba nem aprendendo métodos eficientes
      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?
    • Neste contexto, não parece que a frase tenha sido usada como oposição à otimização em geral
  • 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

    • Vejo a maioria dos ORMs como gerando o schema conforme solicitado. Usar ORM não cria automaticamente um projeto de banco de dados pior do que fazê-lo à mão
      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
    • Também dá para acrescentar aqui a Lei de Conway. Ela diz que “organizações que projetam sistemas acabam produzindo designs que replicam a estrutura de comunicação dessas organizações”
      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
    • Stored procedures vencem
  • 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

    • Aqui se aplicam as regras 1 e 2
  • 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

    • Costumo dizer algo como: “Resolva o problema que você tem. Não resolva o problema que você acha que tem”
    • Esse conceito também se conecta à identificação de desperdícios do Lean e do Six Sigma
      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
    • Indo um nível mais fundo, overengineering surge por se imaginar que talvez a complexidade seja necessária depois e que, naquele momento, expandir o sistema será mais difícil ou arriscado
      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
    • No tratamento de erros, é melhor não tentar ser sofisticado e falhar cedo e de forma simples
  • 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

    • A regra 1 é uma lei férrea para quem não acredita nela, e uma diretriz flexível para quem acredita
      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
    • Algoritmos que parecem lentos podem ser verificados com uma implementação spike. Normalmente, algoritmos lentos são simples de implementar e testar
      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
    • Isso não parece ser uma reação ao texto completo original. Lá, a recomendação é não inserir hacks de velocidade antes de saber onde está o gargalo. É diferente da situação descrita
      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
    • Fico curioso por exemplos concretos. Na maioria dos casos, tenho dúvidas se isso realmente faz diferença
      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 desenvolvedores conseguem prever tão bem o que será lento, então a taxa de sucesso de startups lideradas por desenvolvedores não deveria ser 100%?
      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

    • Não vejo busca binária como um algoritmo sofisticado. As funções modernas de ordenação é que são sofisticadas, e podem ter bugs sutis, então desenvolvedores comuns não deveriam implementá-las por conta própria. Até quicksort tem armadilhas
      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
    • Isso não me parece um contraponto. Do ponto de vista do conselho de Pike, “busca binária em array ordenado” e “objeto BinaryTree” são apenas implementações diferentes da mesma estrutura de dados
    • Não se deve esquecer que, em 99% do tempo, desenvolvedores são o recurso mais caro. Manutenibilidade e velocidade de chegada ao mercado geralmente são muito mais importantes
  • 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

    • Acho que Rob Pike também concordaria que “smart objects” é uma forma equivocada de pensar: https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
    • Levar a lógica “inteligente” para um nível mais alto torna mais fácil entender, testar e alterar. Na minha visão, é muito mais difícil fazer smart objects funcionarem de forma coesa entre si
    • Dá para interpretar como: escreva código que decorre naturalmente de objetos bem estruturados