O caso, como ele chegou
Recentemente acompanhei um caso simples que reforçou algo que venho pensando há tempo: o problema não é usar IA para programar. É usar IA sem entender o que está sendo gerado.
Havia uma migração de dados urgente. O código funcionava, estava em Python, tinha sido escrito rápido e passava nos testes. Lia registros de uma origem, associava cada um a dados de referência, e gravava no destino. Nada exótico. Mas havia um detalhe na forma como ele fazia a associação: em um ponto crítico, para cada registro processado, ele percorria uma coleção inteira procurando a correspondência. Funcionava. Só que fazia isso para cada registro.
Resultado: cerca de quatro registros por segundo.
Pode parecer aceitável. Afinal, funciona, e numa amostra de teste com algumas dezenas de linhas ninguém percebe. Mas faça a conta com o volume real: milhares de registros por tabela, centenas de tabelas. A quatro por segundo, uma migração que deveria caber numa janela de manutenção vira algo que leva dias. E migração que leva dias não é lenta, é inviável, porque a janela fecha, o sistema de origem continua recebendo escrita, e você começa a migrar um alvo que se move.
A forma do problema
O que estava acontecendo tem nome, e é o primeiro capítulo de qualquer curso de estrutura de dados. Associar dois conjuntos percorrendo um deles para cada item do outro é uma operação quadrática: com n registros de um lado e m do outro, são n × m comparações. Dobrou o volume, quadruplicou o tempo. É o tipo de custo que fica invisível em dados pequenos e explode em dados reais, e por isso é tão traiçoeiro: o código que passa no teste e o código que trava a produção são o mesmo código. Só o tamanho da entrada mudou.
A mudança foi simples na estratégia de busca e associação. Em vez de procurar a correspondência de cada registro varrendo a coleção, construir uma vez um índice em memória, um dicionário com a chave de associação, e a partir daí cada busca custa tempo constante. O ponto crítico passou de O(n) por registro para O(1) por registro, e o total de quadrático para linear. A mesma migração passou a processar milhares de registros em minutos.
# a forma do antes: para cada registro, varrer a referência
for reg in registros:
ref = next((r for r in referencias if r.chave == reg.chave), None)
gravar(reg, ref)
# a forma do depois: indexar uma vez, buscar em tempo constante
por_chave = {r.chave: r for r in referencias}
for reg in registros:
gravar(reg, por_chave.get(reg.chave))
E isso não veio de mágica. Veio de base. De entender estrutura de dados. De entender complexidade algorítmica. De saber identificar o gargalo real, em vez de chutar. De uma base construída em estudos e em exercícios de lista, pilha, hash, busca e ordenação que, na época, pareciam distantes de qualquer problema de verdade.
O dicionário troca tempo por memória: a referência inteira precisa caber na RAM do processo que migra. Para centenas de milhares de linhas é trivial; para centenas de milhões, não é, e aí a resposta muda: processar em lotes ordenados pela chave, ou empurrar a associação para o banco, que tem índice e join otimizado há quarenta anos. Também há o custo de construir o índice, que é linear e se paga na primeira centena de buscas. A escolha certa depende do tamanho, e saber disso faz parte do mesmo fundamento que apontou o problema.
"Python é lento" e outras explicações confortáveis
Hoje vejo muita gente dizendo que "Python é lento" ou que "precisa trocar de linguagem" diante de um problema como esse. Às vezes a linguagem importa, e eu não vou fingir que não: há cargas em que a diferença entre Python e uma linguagem compilada é de uma ordem de grandeza e decide o projeto. Mas repare no que aconteceu aqui. A mesma linguagem, o mesmo hardware, a mesma base de dados, e a diferença entre horas e minutos. Nenhuma linguagem do mundo salva um algoritmo quadrático em volume grande; ela só muda a constante. Trocar Python por Go com o mesmo laço aninhado teria dado, talvez, quarenta registros por segundo. Em vez de dias, ainda seriam muitas horas.
A explicação "a linguagem é lenta" é confortável porque transfere a responsabilidade para fora. A explicação "o código foi pensado errado" exige olhar para dentro. E a segunda é a que resolve.
Onde a IA entra nessa história
Vivemos uma fase em que a IA gera código em segundos e entrega respostas boas, funcionais, com nome de variável decente e tratamento de erro. O código do caso era exatamente assim: legível, correto, testado. E tinha um gargalo quadrático no meio, porque o modelo escreveu a forma mais direta de resolver o problema descrito, e a forma mais direta de associar duas coleções é percorrer uma dentro da outra.
Esse é o risco do uso desenfreado. Não é que a IA gere código errado. É que ela gera código que funciona, e "funciona" é um critério fraco demais para sistemas reais. O problema de complexidade não aparece na primeira execução, nem no teste unitário, nem na demonstração. Ele aparece depois:
- Quando a base cresce.
- Quando a migração vai para produção, com o volume de verdade.
- Quando o cliente tem pressa e a janela de manutenção é a madrugada de domingo.
- Quando uma rotina que levava segundos começa a levar horas, e ninguém lembra por quê.
A IA pode gerar dívida técnica em escala industrial justamente porque remove o atrito. Antes, escrever um laço aninhado sobre duas coleções grandes exigia digitar, e enquanto se digitava, algo na cabeça dizia "isso vai ser lento". Agora o código aparece pronto, e a hora de desconfiar passou sem ninguém notar.
IA é uma ferramenta poderosa. Mas não substitui fundamento. Ela acelera quem sabe o que está fazendo. E acelera a dívida técnica de quem não sabe. A velocidade é a mesma nos dois casos; só a direção muda.
O que mudaria o desfecho
Olhando para trás, o gargalo teria sido pego por qualquer uma de três coisas, e nenhuma delas é sofisticada:
- Um teste com volume representativo. Não um teste de carga formal; bastava rodar a migração numa amostra de alguns milhares de registros e olhar o relógio. A curva quadrática se denuncia em segundos.
- Um profiler. Dez minutos com o profiler padrão do Python apontam a linha que consome tudo. Medir antes de otimizar é a regra mais repetida da engenharia e a mais ignorada.
- A revisão com as perguntas certas. Não "isso funciona?", que o teste responde, mas as perguntas que o teste não responde.
E talvez seja aí que está o diferencial do desenvolvedor daqui para a frente. Não vai ser pedir código para a IA, porque isso todo mundo faz. Vai ser olhar para o código que veio e perguntar:
- "Isso está correto?"
- "Isso escala?"
- "Onde está o gargalo?"
- "Qual é a complexidade disso?"
- "O que acontece quando a entrada for cem vezes maior?"
Essas perguntas não exigem genialidade. Exigem fundamento, e exigem o hábito de fazê-las mesmo quando o código parece bom. Principalmente quando parece bom, porque código que parece bom não é revisado com atenção.
Trataria código gerado exatamente como trato código de um colega júnior competente: confio na sintaxe, confio na intenção, e reviso a complexidade, os limites e o comportamento com dados grandes. Não por desconfiança da ferramenta. Por respeito ao sistema que vai rodar aquilo em produção.
Resumo da conversa
- Um código correto e testado rodava a quatro registros por segundo por causa de uma associação quadrática. Trocar a varredura por um índice em memória levou a migração de dias para minutos.
- Linguagem muda a constante; algoritmo muda a curva. "Python é lento" era a explicação confortável e errada.
- A IA não gerou código errado. Gerou código que funciona, e funcionar não basta. O custo aparece quando a base cresce.
- Teste com volume, profiler e revisão com as perguntas certas pegariam o problema em minutos.
- Código que funciona não é necessariamente código bom. Em sistemas reais, essa diferença custa tempo, dinheiro e confiança.
Igor Nascimento