SQLite parse_number(): C supera Rust após reescrita do analisador
O experimento de Simon Willison com SQLite parse_number() inicialmente colocou Rust à frente de C, até que uma reescrita direcionada do analisador inverteu o ranking de desempenho. Publicada em 8 de novembro de 2026, a investigação compara extensões, complementos compilados que introduzem funções de banco de dados, para limpar números formatados. A implementação resultante em C teria liderado todos os conjuntos de dados testados, mantendo um arquivo compilado menor.
A tarefa parece modesta: transformar texto com símbolos monetários, vírgulas ou sinais de porcentagem em valores adequados para ordenação numérica e soma. Ainda assim, a comparação expôs respostas diferentes para diferentes questões de engenharia. Rust entregou o analisador decimal inicial mais rápido; C otimizado acabou entregando a implementação mais rápida medida.
Para desenvolvedores que avaliam código gerado por IA, a pressão recai sobre o processo de revisão. Uma implementação plausível e uma linguagem conhecida fornecem pouca evidência sobre onde o tempo de execução é gasto. A investigação de Willison se tornou útil quando a medição identificou uma rotina de conversão específica e estimulou um experimento mais restrito.
SQLite parse_number() muda o resultado após uma segunda etapa
O vencedor relatado mudou quando a implementação mudou, tornando o design do analisador central para a comparação.
Willison começou com um pedido ao Claude, identificado em sua conta como Opus 5.5, para explorar a função em C e Rust. A comparação solicitada cobria velocidade de execução, complexidade do código e o tamanho da biblioteca resultante. Ele descreve a investigação em seu experimento de análise no SQLite.
Os primeiros resultados separaram tamanho de arquivo de velocidade. Willison relatou uma extensão em C de aproximadamente 14 KB, em comparação com 321 KB usando rusqlite e 298 KB usando sqlite-loadable-rs. Essas bibliotecas Rust conectam código Rust ao SQLite. Apesar de seu arquivo menor, a implementação inicial em C perdeu em entradas mistas e decimais.
A explicação se concentrou em strtod(), uma função da biblioteca C que converte texto em um número de ponto flutuante, uma representação numérica aproximada. Willison atribuiu a vantagem decimal inicial de Rust à sua rotina de conversão mais rápida. Em seguida, pediu ao Claude que produzisse um analisador em C que igualasse ou superasse a implementação em Rust.
A versão revisada em C liderou a comparação relatada. Em dados mistos, os custos líquidos arredondados caíram de 67 nanossegundos por linha para o C inicial para 19 no C revisado. As versões em Rust registraram 46 e 41. Entradas exclusivamente decimais mostraram uma mudança ainda maior dentro de C, de 106 para 18 nanossegundos por linha.
Essa sequência dá relevância à história. Parar após a primeira comparação sustentaria a escolha de Rust para a carga de trabalho decimal medida. Prosseguir com uma revisão direcionada produziu um candidato diferente, sem alterar a função de banco de dados solicitada.
O exercício também separa gerar alternativas de avaliá-las. Claude forneceu implementações, mas seu valor relativo emergiu por meio das medições ao redor delas. A resposta inicial forneceu um ponto de partida, e não uma decisão de engenharia definitiva.
Não há base aqui para tratar o resultado como um ranking universal de linguagens de programação. Trata-se de implementações específicas de uma função pequena. Suas diferenças incluem rotinas de conversão, armazenamento temporário e código de integração, todos abaixo de uma manchete ampla de C versus Rust.
A mudança prática é mais específica: os desenvolvedores têm um exemplo publicado em que um primeiro rascunho lento em C se tornou o candidato mais rápido medido. Isso torna o caso útil para examinar escolhas de implementação, ao mesmo tempo que deixa alegações mais amplas sobre linguagens sem sustentação.
O benchmark mede análise repetida dentro do SQLite
A carga de trabalho dá à conversão repetida um papel central, portanto seus resultados precisam ser considerados nesse contexto.
O harness de benchmark publicado cria um banco de dados em memória contendo 2 milhões de linhas. Ele avalia uma consulta que analisa cada valor e soma os resultados. Python inicia a consulta, enquanto o SQLite executa o trabalho repetido no banco de dados.
Esse arranjo importa porque evita colocar um loop Python em torno de cada conversão individual. Portanto, o experimento visa mais diretamente a chamada combinada da função de banco de dados e o caminho de análise. Ele não mede principalmente uma aplicação Python cruzando repetidamente para código nativo.
O harness alterna candidatos ao longo de 15 execuções. Ele registra tempos mínimos e medianos decorrido, depois calcula o valor líquido anunciado a partir do mínimo. Uma varredura de referência que soma comprimentos de strings é subtraída antes que o resultado se torne nanossegundos por linha.
Essa subtração ajuda a isolar o trabalho incremental, mas o número continua sendo uma estimativa comparativa. Não é a duração completa de uma importação, atualização de painel ou consulta envolvendo outras operações. Interpretá-lo como um tempo de resposta voltado ao usuário extrapolaria o desenho da medição.
Os conjuntos de dados também moldam o resultado. Execuções separadas abrangem texto de inteiros, texto decimal e valores mistos formatados. O gerador misto inclui valores com marcação monetária, porcentagens, strings numéricas comuns e entradas inválidas. Essas variações permitem distinguir um problema de conversão decimal de um problema geral de análise.
Considere uma aplicação que ordena repetidamente relatórios financeiros importados. Se a análise ocorre durante cada consulta, o trabalho de conversão passa a fazer parte de cada solicitação de relatório. Se a aplicação, em vez disso, normaliza valores durante a importação, o mesmo analisador afeta uma etapa diferente e enfrenta uma exigência de desempenho diferente.
Nenhuma das arquiteturas decorre automaticamente dessas medições. O experimento fornece evidência sobre o componente de análise, enquanto a aplicação determina a frequência com que esse componente é executado. Uma função isolada mais rápida importa mais quando ocupa uma parcela significativa da carga de trabalho real.
A alternativa em SQL puro do benchmark usa chamadas replace() aninhadas, ou seja, substituições repetidas de strings, seguidas de conversão numérica. Sua presença oferece aos desenvolvedores um ponto de comparação familiar. No entanto, substituir caracteres e impor uma política completa de entrada são trabalhos diferentes, portanto um comportamento equivalente exige exame separado.
A pressão, portanto, recai sobre desenvolvedores que usam resumos curtos de benchmarks para justificar escolhas de implementação. Antes de adotar o ranking relatado, eles precisam identificar onde a análise ocorre em sua aplicação. Também precisam determinar se suas entradas se assemelham aos conjuntos de dados gerados.
Para SQLite parse_number(), o harness torna essas questões verificáveis. Os leitores podem examinar tanto a consulta cronometrada quanto o gerador de dados, em vez de confiar apenas em um número vencedor.
C ganha velocidade ao combinar validação e conversão
A implementação otimizada em C reduz o trabalho intermediário, mantendo uma alternativa para entradas fora dos limites de seu atalho.
O analisador decimal otimizado inspeciona caracteres enquanto acumula informação numérica. Durante essa varredura, ele acompanha dígitos significativos e conta dígitos após o ponto decimal. Isso permite que entradas adequadas evitem a reconstrução como uma string limpa.
Seu atalho usa o caminho rápido de Clinger, uma técnica de conversão decimal aplicada quando valores intermediários cumprem requisitos de representação exata. A implementação restringe esse caminho usando condições numéricas explícitas. Valores fora dessas condições seguem por uma rota de conversão mais lenta.
Essa distinção é essencial. Um atalho que simplesmente aceitasse erros de arredondamento responderia a uma questão de engenharia diferente. Aqui, o código tenta acelerar um conjunto restrito de entradas enquanto preserva o resultado numérico pretendido.
A implementação coleta até 19 dígitos significativos. Seu atalho decimal exige adicionalmente um valor acumulado não superior a 2^53 e no máximo 22 dígitos fracionários. Esses limites determinam quando o cálculo direto é elegível.
O design mais amplo explica por que a revisão mudou a comparação. A validação já exige examinar os caracteres da entrada. Acumular informação numérica útil durante esse exame evita parte do trabalho que uma etapa de conversão separada, de outra forma, repetiria.
O tratamento de inteiros se beneficia da mesma abordagem. Quando o valor cabe, a função pode retornar diretamente o inteiro acumulado. Ela não precisa construir uma string limpa temporária antes de converter essa string novamente.
A alternativa continua sendo trabalho de engenharia substancial. Ela reconstrói texto numérico validado, usa um buffer local quando possível e aloca armazenamento adicional quando necessário. Alocação aqui significa solicitar memória temporária para a operação, o que introduz tratamento além da própria aritmética.
O código também verifica o comportamento de avaliação de ponto flutuante do compilador antes de habilitar o atalho. Sua proteção aborda a possibilidade de cálculos intermediários usarem uma precisão diferente. Esse detalhe mostra como uma pequena otimização aritmética pode assumir obrigações além de sua aparente contagem de linhas.
Rust continua sendo uma comparação relevante porque seus candidatos usam um arranjo de trabalho diferente. A investigação não estabelece que uma reescrita em Rust igualmente especializada manteria o mesmo tempo. Comparar C otimizado com essas versões em Rust estabelece um resultado para esses candidatos.
Uma interpretação razoável é que a especialização compensou para a carga de trabalho testada. A reescrita em C combina tarefas que antes eram separadas e evita uma rotina de conversão geral para valores elegíveis. Essa interpretação decorre do mecanismo visível na implementação.
Também explica por que a reputação de uma linguagem fornece uma previsão incompleta. Rotinas gerais oferecem comportamento para um amplo espaço de entrada; código especializado pode visar um contrato mais estreito. O desempenho depende, em parte, de quanto trabalho a rota escolhida realiza para as entradas que realmente chegam.
A questão de manutenção segue diretamente. Quem for responsável pela implementação mais rápida também será responsável por suas condições de elegibilidade, comportamento de alternativa e premissas numéricas. A vantagem medida só se torna útil se a equipe puder preservar essas propriedades em mudanças futuras.
Tamanho da extensão e propriedade do código apontam em direções diferentes
O artefato compilado menor e o caminho de manutenção mais simples são critérios separados, e nenhum substitui o outro.
A comparação de implementações do projeto relata bibliotecas C removidas de símbolos com 14.408 bytes. Os artefatos Rust correspondentes medem 320.864 bytes com rusqlite e 298.464 bytes com sqlite-loadable. A remoção de símbolos elimina determinadas informações de compilação de um arquivo compilado.
O mesmo relatório conta 75 linhas não vazias e não comentadas para C direto e 104 para C otimizado. Rust usa 36 ou 30 linhas de integração, além de um analisador compartilhado de 41 linhas. Ele relata 20 dependências para a versão rusqlite e 62 para sqlite-loadable.
Essas medições descrevem formas diferentes de complexidade. O tamanho do código-fonte captura o volume visível da implementação; a contagem de dependências captura parte do grafo de compilação. Nenhum mede diretamente quão difícil será diagnosticar um bug futuro.
Uma função curta pode depender de um comportamento distribuído entre bibliotecas. Uma função mais longa e autocontida pode concentrar a responsabilidade em código que a equipe da aplicação precisa revisar. Escolher entre elas exige perguntar onde a equipe quer que essa responsabilidade esteja.
A implementação em C torna várias responsabilidades visíveis. Seu tratamento de buffer, verificações de intervalo de inteiros e atalho decimal aparecem no próprio código-fonte do projeto. Revisores podem inspecioná-los diretamente, mas a visibilidade não elimina a necessidade de especialização.
Os recursos da linguagem Rust e sua biblioteca de parsing podem reduzir parte do tratamento manual. Isso pode melhorar a legibilidade para uma equipe que já trabalha com Rust. No entanto, uma camada de integração compacta ainda precisa ser revisada quanto à política de entrada, ao comportamento do banco de dados e ao tratamento de erros.
O tamanho compilado cria outra questão. Uma biblioteca adicional tem significado diferente em uma distribuição com restrições severas do que em uma aplicação que já carrega ativos substanciais. A diferença relatada é mensurável; sua importância prática depende do ambiente de distribuição.
O tamanho do arquivo também não pode representar o consumo de memória em tempo de execução. Os bytes de uma biblioteca em disco não descrevem todas as alocações realizadas durante o processamento de dados. Tratar o menor arquivo como a implementação de menor uso de memória exigiria medições que esta comparação não fornece.
A mesma cautela se aplica às contagens de dependências. Mais dependências podem introduzir mais considerações de compilação e atualização, mas contá-las não estabelece taxas de defeitos. Da mesma forma, menos dependências não estabelecem que o código pertencente à aplicação seja mais fácil de manter.
Para uma aplicação C existente, uma extensão C compacta pode se adequar às práticas estabelecidas de compilação e revisão. Para uma aplicação Rust existente, introduzir um componente C separado pode criar trabalho adicional de responsabilidade. São julgamentos de implantação, e não resultados estabelecidos pelo benchmark.
SQLite parse_number() apresenta, portanto, uma decisão com diversas dimensões independentes. O candidato C otimizado relatado lidera em parsing medido e tamanho compilado. Ainda assim, uma equipe precisa decidir se manter seu caminho de conversão especializado se ajusta às suas competências e ao seu processo de lançamento.
A comparação útil mantém essas dimensões visíveis. Reduzi-las a um único vencedor esconderia as razões pelas quais equipes diferentes podem, de forma razoável, fazer escolhas diferentes.
Passar nos testes não resolve o contrato de dados
A correção inclui o significado atribuído ao texto formatado, além de as implementações concordarem em uma resposta numérica.
O relatório de comparação afirma que todas as versões passaram por 40 casos explícitos e 8 milhões de comparações aleatórias. Testes aleatórios, frequentemente chamados de fuzzing, geram entradas para procurar comportamentos inesperados. Esses resultados relatados fornecem evidências, embora não tenham sido reproduzidos de forma independente para este artigo.
A concordância entre implementações é valiosa, especialmente após uma reescrita aritmética. Ela torna divergências óbvias menos prováveis dentro do espaço de entrada testado. Ainda assim, a concordância compartilhada não pode determinar se o comportamento selecionado corresponde às regras de negócio de uma aplicação específica.
Percentuais ilustram a distinção. Remover um sinal de porcentagem preserva o número exibido, enquanto interpretar uma porcentagem como proporção altera sua escala. Uma aplicação precisa decidir de qual significado necessita antes de considerar qualquer um dos comportamentos correto.
Marcações de moeda levantam uma questão relacionada. Remover um símbolo torna o texto mais fácil de processar numericamente, mas não estabelece que os valores compartilham a mesma unidade. Combinar valores de moedas diferentes requer informações que a simples remoção de caracteres não pode fornecer.
As convenções de formatação também merecem atenção. Uma vírgula pode indicar agrupamento de dígitos em uma convenção e uma separação decimal em outra. Um parser projetado em torno de uma convenção fixa precisa ter essa expectativa associada aos dados que recebe.
Estas são preocupações hipotéticas de adoção, não defeitos estabelecidos na implementação de Willison. Elas explicam por que um parser pode funcionar exatamente como planejado e ainda assim ser inadequado para um conjunto de dados desconhecido. A correção começa com o contrato selecionado.
A precisão numérica acrescenta outro limite. A documentação de ponto flutuante do SQLite explica que seus valores REAL usam representação binária de 64 bits e são aproximados. Muitas frações decimais não têm representação exata nesse formato, independentemente da linguagem que realiza a conversão.
Um parser corretamente arredondado seleciona o valor representável apropriado. Ele não torna exata toda a aritmética subsequente. Aplicações que exigem cálculos decimais exatos precisam avaliar separadamente seus requisitos de representação e aritmética.
O tratamento de falhas merece a mesma atenção. Retornar NULL, o marcador do SQLite para um valor ausente ou desconhecido, pode permitir que o processamento continue quando o texto é inválido. Ainda assim, uma aplicação deve preservar uma forma de distinguir dados rejeitados de entradas numéricas válidas.
Caso contrário, um resultado aparentemente limpo pode ocultar um problema de qualidade dos dados. Um usuário que examina um total precisa saber se cada registro esperado contribuiu de forma significativa. Essa preocupação pertence à aplicação ao redor, mesmo quando o parser segue seu contrato.
O próprio benchmark introduz uma incerteza separada. O tempo mínimo após a subtração da linha de base descreve uma execução medida favorável. Ele não estabelece o desempenho típico sob contenção de produção, conjuntos de dados variados ou um ambiente computacional diferente.
Também não decorre uma afirmação universal de desempenho do resultado compartilhado dos testes. Uma carga de trabalho com mais entradas excepcionalmente longas poderia acionar o fallback com mais frequência. Isso alteraria o equilíbrio do trabalho em relação aos valores de formatação comum testados.
Para SQLite parse_number(), uma leitura cética é, portanto, construtiva. Os resultados justificam examinar uma implementação específica e reproduzir seu comportamento. Eles não eliminam a necessidade de definir formatos aceitos, preservar registros rejeitados e verificar os resultados na aplicação hospedeira pretendida.
Acompanhe a reprodução, revisões em Rust e o comportamento do fallback
As próximas evidências úteis ampliariam a comparação, mantendo explícitos o comportamento e os métodos de medição.
O primeiro sinal a acompanhar é a reprodução independente. Uma execução em outro sistema testaria quão bem a ordenação relatada resiste a mudanças no hardware e no ambiente de compilação. Relatar tanto os tempos completos de consulta quanto os custos derivados de parsing tornaria essas evidências mais fáceis de interpretar.
Resultados correspondentes fortaleceriam a confiança de que a vantagem do C otimizado se estende além da configuração original. Uma ordenação diferente restringiria a conclusão e identificaria outro fator que vale investigar. Qualquer um dos resultados melhoraria a comparação se o ambiente e a distribuição das entradas fossem documentados.
A reprodução também deve preservar a distinção entre resultados numéricos e tempo. Um candidato rápido que altera as entradas aceitas já não responde à mesma pergunta. Publicar os resultados de correção junto com o desempenho manteria esse limite visível.
O segundo sinal é uma implementação Rust revisada que aborde o trabalho específico visível na comparação. Uma correspondência mais próxima na estratégia de varredura e conversão ajudaria a separar a estrutura da implementação da escolha de linguagem. A pergunta relevante é se uma especialização comparável reduz a diferença medida.
Se o Rust revisado reduzir a diferença enquanto preserva seus benefícios de integração, o resultado inicial se torna um argumento mais forte para examinar algoritmos antes de selecionar linguagens. Se o C otimizado mantiver a liderança, os desenvolvedores obterão evidências mais firmes para este componente específico.
Isso continua sendo uma comparação proposta, não um lançamento anunciado ou resultado prometido. Não há motivo para supor que uma versão atualizada aparecerá em um cronograma fixo. Seu valor viria de tornar as implementações concorrentes mais diretamente comparáveis.
O terceiro sinal são evidências sobre o comportamento do fallback. O atalho C otimizado trata rapidamente entradas elegíveis, mas distribuições de entrada mais amplas podem alterar a frequência com que a rota mais lenta é executada. Testar esses casos revelaria mais sobre a faixa prática de operação da implementação.
Esse trabalho deve examinar a correção tão de perto quanto a velocidade. Cadeias numéricas longas, registros malformados e diferentes configurações do host podem expor suposições que valores comuns formatados não expõem. Um resultado documentado fortaleceria mais a confiança do que outro tempo de destaque isolado.
Para equipes de engenharia, preservar as evidências ao redor importa tanto quanto preservar o código-fonte. Configurações de compilação, exemplos de entrada e alternativas rejeitadas explicam por que uma implementação específica foi escolhida. Mantê-los em uma base de conhecimento pesquisável oferece aos futuros revisores uma forma de revisitar essa decisão.
O valor imediato da investigação já está claro: um gargalo medido levou a uma revisão específica e a um resultado alterado. O que permanece em aberto é quão amplamente esse resultado se aplica. Comparações futuras devem ampliar as evidências sem transformar um experimento focado em um veredicto universal sobre linguagens.
Antes de adotar SQLite parse_number(), identifique a decisão que sua aplicação realmente enfrenta. Preserve entradas representativas, defina como percentuais e registros inválidos devem se comportar e compare os candidatos dentro do host que os executará. Mantenha os tempos completos de consulta ao lado de quaisquer números derivados de parsing, para que um resultado favorável de componente permaneça conectado à experiência do usuário. Revise o caminho numérico especializado com o mesmo cuidado dispensado ao seu fallback e retenha os detalhes de compilação necessários para reproduzir ambos. O próximo passo útil é testar se as vantagens desta implementação resistem aos seus requisitos. Qual restrição decide a escolha em sua aplicação: tempo de conversão repetida, tamanho da distribuição ou o esforço necessário para manter o parser?



