570 Mil Linhas de Código Gerado por LLM Compilaram Perfeitamente. Era 20.171x Mais Lento que o SQLite.
Alguém fez benchmark de uma reimplementação do SQLite em Rust escrita por um LLM. A diferença entre código que parece certo e código que é certo foi de cinco ordens de magnitude.
Uma reimplementação do SQLite em Rust, escrita inteiramente por um LLM, foi submetida recentemente a um benchmark. Compilou. Os testes passaram. O código era limpo, bem estruturado e idiomático em Rust. Em uma busca simples por chave primária, era 20.171 vezes mais lento que o SQLite.
Esse número me parou. Não porque código gerado por LLM ser lento seja surpresa, mas por causa de onde veio essa lentidão. O código não estava errado em nenhum sentido que um compilador ou suite de testes conseguiria capturar. A B-tree estava corretamente implementada. O query planner existia. A storage engine funcionava. Cada peça era individualmente defensável. O sistema como um todo era quase inutilizável.
Passei um tempo lendo a análise do benchmark e o código-fonte. Os padrões que encontrei aparecem repetidamente em projetos gerados por LLMs, e acredito que apontam para algo fundamental sobre como esses modelos escrevem código.
A B-tree estava lá. O query planner a ignorava.
No SQLite, uma busca por PRIMARY KEY usa o caminho da B-tree e termina em tempo O(log n). Quatro linhas no where.c verificam o iPKey e roteiam a query diretamente para a árvore. Essa é uma daquelas micro-otimizações que só faz sentido se você entende como o sistema inteiro se encaixa.
A versão gerada pelo LLM também tinha uma implementação de B-tree. Funcionava corretamente de forma isolada. O problema era que o query planner nunca a chamava para buscas por chave primária. A função is_rowid_ref() reconhecia apenas três strings literais: “rowid”, “rowid” e “oid”. Se você declarasse uma coluna como id INTEGER PRIMARY KEY, o planner não reconhecia isso como alias de rowid. Toda query caía em um full table scan.
A matemática aqui é brutal. Para 100 linhas consultadas 100 vezes, o caminho da B-tree exige cerca de 700 passos de comparação. O caminho do full scan passa de 10.000. Mas o estrago real vem da complexidade algorítmica: O(log n) por busca vira O(n), e ao longo de todo o benchmark suite, isso vai se acumulando até chegar ao gap de 20.171x.
Esse é o tipo de bug que nenhum teste unitário captura a não ser que você escreva especificamente um benchmark. A B-tree funciona. O scan funciona. O planner escolhe o caminho errado. Tudo passa.
Defaults seguros se acumulam como juros compostos
O que tornava esse caso mais interessante do que um bug simples de roteamento era o seguinte: mesmo descontando o problema do query planner, a reimplementação ainda era aproximadamente 2.900 vezes mais lenta. O gap restante vinha de uma pilha de decisões individualmente razoáveis.
Cada execução de query clonava a AST completa e a recompilava para bytecode. O SQLite reutiliza handles de prepared statements. As duas abordagens são válidas, mas clonar uma AST em toda execução é caro em escala.
Cada leitura de página alocava um novo buffer de 4KB no heap. O page cache do SQLite retorna um ponteiro direto para memória já carregada. A versão do LLM escolheu o caminho seguro e óbvio: alocar, ler, retornar. Funciona. Só é ordens de magnitude mais lento quando você está lendo milhares de páginas por query.
Cada commit reconstruía o schema inteiro do zero. O SQLite compara um único valor inteiro de cookie. Se o cookie não mudou, o schema ainda é válido. A reimplementação não tinha esse conceito, então fazia o trabalho completo toda vez.
Cada statement disparava uma chamada sync_all() para dar flush em todos os metadados do arquivo no disco. O SQLite usa fdatasync(), que só dá flush nos dados do arquivo e pula o sync de metadados. A diferença é enorme em workloads de escrita intensiva.
Quero chamar isso de efeito composto dos defaults defensivos. Cada escolha isolada tem uma justificativa razoável. Clonar a AST evita complexidade de ownership em Rust. Alocar buffers novos previne bugs de use-after-free. Reconstruir o schema evita problemas de cache stale. Chamar sync_all() oferece a garantia de durabilidade mais forte.
Mas custos de performance se multiplicam, não se somam. Quando quatro penalidades de 10x se acumulam, você não fica 40x mais lento. Você fica 10.000x mais lento. Um LLM não raciocina sobre esse efeito composto porque gera cada função de forma relativamente isolada. Otimiza localmente e paga globalmente.
82.000 linhas para substituir um one-liner de cron
O outro projeto gerado por LLM do mesmo desenvolvedor mostrou o mesmo padrão de uma forma diferente. O problema: artifacts de build no diretório target/ do Rust consomem espaço em disco ao longo do tempo. A solução do LLM: um daemon em Rust com 82.000 linhas, sete dashboards e uma engine de scoring Bayesiana para decidir quais artifacts limpar.
A solução existente é find ./target -type f -atime +30 -delete, uma única linha em um cron job. Zero dependências. Ou então o cargo-sweep, uma ferramenta oficial da comunidade que já existe e lida com edge cases que o daemon não cobre.
O projeto gerado pelo LLM puxou 192 dependências. Para referência, o ripgrep, uma das ferramentas de busca mais sofisticadas do ecossistema Rust, usa 61.
Esse é um padrão que continuo vendo: LLMs constroem o que você pede, não o que você precisa. Se você pede “construa um sistema que gerencie de forma inteligente os artifacts de build do Rust com monitoramento e scoring”, você recebe exatamente isso. O modelo não tem mecanismo para recuar e perguntar se o problema requer um sistema sequer. Ele não sabe que o tamanho do diretório target/ é uma reclamação perene na comunidade Rust com soluções bem conhecidas. Não considera o custo de manutenção de 192 dependências versus zero.
As pesquisas apontam na mesma direção
Fiquei curioso se esses dois projetos eram outliers, então olhei para as pesquisas mais amplas. Não são.
A METR conduziu um ensaio clínico randomizado com 16 desenvolvedores experientes de open source. O grupo que usava ferramentas de IA completou as tarefas 19% mais devagar que o grupo de controle. O que ficou na minha cabeça: depois que o experimento terminou, o grupo de IA acreditava ter sido 20% mais rápido. A experiência subjetiva de produtividade era o inverso da realidade medida.
A GitClear analisou 210 milhões de linhas de código e descobriu que código colado passou pela primeira vez a superar código refatorado. A tendência se correlaciona diretamente com a adoção de ferramentas de programação com IA. Código está sendo adicionado mais rápido do que está sendo melhorado.
O relatório DORA 2024 do Google constatou que um aumento de 25% na adoção de IA se correlacionou com uma queda de 7,2% na estabilidade de deployment. Mais código gerado por IA indo para produção, mais incidentes saindo.
O benchmark Mercury do NeurIPS 2024 adicionou métricas de eficiência aos benchmarks padrão de coding. Quando você mede não apenas “produz output correto” mas “produz output correto sem desperdiçar recursos”, as taxas de aprovação caíram abaixo de 50%.
Nada disso significa que LLMs são inúteis para programar. Eu os uso constantemente. Mas significa que “compila e passa nos testes” é um bar perigosamente baixo. O gap entre código plausível e código correto é onde acontece a engenharia de verdade.
O que isso exige dos desenvolvedores na prática
O problema central não é que LLMs escrevem código ruim. Eles escrevem código localmente coerente e globalmente incoerente. Cada função faz sentido. O sistema não faz. Esse é exatamente o modo de falha que os testes tradicionais não capturam, porque testes verificam comportamento local.
O que se faz necessário é uma avaliação que mire nas lacunas. Benchmarks, não só testes. Performance budgets no CI, não apenas verificações de corretude. Revisão arquitetural que pergunte “por que esse módulo existe” antes de verificar se ele funciona. Auditorias de dependências que comparam a complexidade da solução com a complexidade do problema.
A pergunta não é “esse código parece certo?” É “como provamos que está certo?” E provar isso exige o tipo de pensamento sistêmico que LLMs atualmente não têm.
O gap entre o que você pediu e o que a produção exige é onde vive o julgamento de engenharia. Sem medição, geração de código é só geração de tokens.
Assine a newsletter
Receba atualizações sobre meus projetos mais recentes, artigos e experimentos com IA e desenvolvimento web.