SQLite parse_number(): il C supera Rust dopo la riscrittura di un parser
L’esperimento di Simon Willison su SQLite parse_number() aveva inizialmente visto Rust davanti al C, finché una riscrittura mirata del parser non ha invertito la classifica delle prestazioni. Pubblicata l’8 novembre 2026, l’indagine confronta estensioni, componenti aggiuntivi compilati che introducono funzioni di database, per ripulire numeri formattati. Secondo quanto riportato, l’implementazione C risultante ha guidato ogni set di dati testato, mantenendo al contempo un file compilato più piccolo.
Il compito sembra modesto: trasformare testo contenente simboli di valuta, virgole o segni di percentuale in valori adatti all’ordinamento numerico e all’addizione. Eppure il confronto ha prodotto risposte diverse a diverse domande ingegneristiche. Rust ha fornito il parser decimale iniziale più veloce; il C ottimizzato ha infine fornito l’implementazione più rapida tra quelle misurate.
Per gli sviluppatori che valutano codice generato dall’IA, la pressione ricade sul processo di revisione. Un’implementazione plausibile e un linguaggio familiare offrono poche prove su dove venga impiegato il tempo di esecuzione. L’indagine di Willison è diventata utile quando le misurazioni hanno identificato una routine di conversione specifica e sollecitato un esperimento più circoscritto.
SQLite parse_number() cambia il risultato dopo un secondo passaggio
Il vincitore riportato è cambiato quando è cambiata l’implementazione, rendendo il design del parser centrale nel confronto.
Willison ha iniziato con una richiesta a Claude, identificato nel suo account come Opus 5.5, di esplorare la funzione in C e Rust. Il confronto richiesto copriva velocità di esecuzione, complessità del codice e dimensioni della libreria risultante. Descrive l’indagine nel suo esperimento sul parsing in SQLite.
I primi risultati hanno separato le dimensioni del file dalla velocità. Willison ha riportato un’estensione C di circa 14 KB, rispetto a 321 KB con rusqlite e 298 KB con sqlite-loadable-rs. Queste librerie Rust collegano il codice Rust a SQLite. Nonostante il file più piccolo, l’implementazione C iniziale perdeva con input misti e decimali.
La spiegazione si concentrava su strtod(), una funzione della libreria C che converte testo in un numero a virgola mobile, una rappresentazione numerica approssimata. Willison ha attribuito il vantaggio iniziale di Rust sui decimali alla sua routine di conversione più veloce. Ha quindi chiesto a Claude di produrre un parser C che eguagliasse o superasse l’implementazione Rust.
La versione C rivista ha guidato il confronto riportato. Su dati misti, i costi netti arrotondati sono scesi da 67 nanosecondi per riga per il C iniziale a 19 per il C rivisto. Le versioni Rust hanno registrato 46 e 41. Gli input esclusivamente decimali hanno mostrato un cambiamento ancora maggiore all’interno del C, da 106 a 18 nanosecondi per riga.
Questa sequenza conferisce rilevanza alla storia. Fermarsi dopo il primo confronto avrebbe sostenuto la scelta di Rust per il carico di lavoro decimale misurato. Proseguire con una revisione mirata ha prodotto un candidato diverso, senza modificare la funzione di database richiesta.
L’esercizio separa anche la generazione di alternative dalla loro valutazione. Claude ha fornito le implementazioni, ma il loro valore relativo è emerso attraverso le misurazioni circostanti. La risposta iniziale ha fornito un punto di partenza, non una decisione ingegneristica definitiva.
Non vi sono basi per trattare l’esito come una classifica universale dei linguaggi di programmazione. Si tratta di implementazioni particolari di una piccola funzione. Le loro differenze includono routine di conversione, archiviazione temporanea e codice di integrazione, tutti elementi che si collocano sotto un ampio titolo C contro Rust.
Il cambiamento pratico è più specifico: gli sviluppatori hanno un esempio pubblicato in cui una prima bozza C lenta è diventata il candidato più veloce tra quelli misurati. Questo rende il caso utile per esaminare le scelte di implementazione, lasciando tuttavia prive di supporto affermazioni più ampie sui linguaggi.
Il benchmark misura il parsing ripetuto all’interno di SQLite
Il carico di lavoro attribuisce un ruolo centrale alla conversione ripetuta, quindi i suoi risultati richiedono questo contesto.
Il benchmark harness pubblicato crea un database in memoria contenente 2 milioni di righe. Valuta una query che analizza ogni valore e somma i risultati. Python avvia la query, mentre SQLite esegue il lavoro ripetuto sul database.
Questa configurazione è importante perché evita di collocare un ciclo Python attorno a ogni singola conversione. L’esperimento punta quindi più direttamente alla chiamata combinata della funzione di database e al percorso di parsing. Non misura principalmente un’applicazione Python che attraversa ripetutamente il confine verso codice nativo.
L’harness alterna i candidati in 15 esecuzioni. Registra i tempi trascorsi minimi e mediani, poi calcola il dato netto pubblicizzato a partire dal minimo. Una scansione di riferimento che somma le lunghezze delle stringhe viene sottratta prima che il risultato diventi nanosecondi per riga.
Questa sottrazione aiuta a isolare il lavoro incrementale, ma il dato resta una stima comparativa. Non è la durata completa di un’importazione, dell’aggiornamento di una dashboard o di una query che coinvolga altre operazioni. Interpretarlo come tempo di risposta per l’utente estenderebbe la misurazione oltre il suo design.
Anche i set di dati modellano il risultato. Esecuzioni separate coprono testo intero, testo decimale e valori formattati misti. Il generatore misto include valori con simboli di valuta, percentuali, stringhe numeriche ordinarie e voci non valide. Queste variazioni consentono di distinguere un problema di conversione decimale da un problema generale di parsing.
Si consideri un’applicazione che ordina ripetutamente report finanziari importati. Se il parsing avviene durante ogni query, il lavoro di conversione diventa parte di ogni richiesta di report. Se invece l’applicazione normalizza i valori durante l’importazione, lo stesso parser influenza una fase diversa e affronta un requisito prestazionale differente.
Nessuna delle due architetture deriva automaticamente da queste misurazioni. L’esperimento fornisce evidenza sulla componente di parsing, mentre l’applicazione determina con quale frequenza quella componente viene eseguita. Una funzione isolata più veloce conta soprattutto quando occupa una quota significativa del carico di lavoro effettivo.
L’alternativa SQL pura del benchmark usa chiamate replace() annidate, ossia sostituzioni ripetute di stringhe, seguite dalla conversione numerica. La sua presenza offre agli sviluppatori un punto di confronto familiare. Tuttavia, sostituire caratteri e applicare una politica completa sugli input sono compiti diversi, quindi il comportamento equivalente richiede un esame separato.
La pressione ricade quindi sugli sviluppatori che usano brevi sintesi dei benchmark per giustificare le scelte di implementazione. Prima di adottare la classifica riportata, devono identificare dove avviene il parsing nella loro applicazione. Devono anche stabilire se i loro input assomigliano ai set di dati generati.
Per SQLite parse_number(), l’harness rende queste domande verificabili. I lettori possono esaminare sia la query cronometrata sia il generatore di dati, anziché affidarsi soltanto a un numero vincente.
Il C guadagna velocità combinando validazione e conversione
L’implementazione C ottimizzata riduce il lavoro intermedio mantenendo un fallback per gli input che superano i limiti della sua scorciatoia.
Il parser decimale ottimizzato ispeziona i caratteri mentre accumula informazioni numeriche. Durante questa scansione, traccia le cifre significative e conta le cifre che seguono il separatore decimale. Questo consente agli input adatti di evitare la ricostruzione come stringa ripulita.
La sua scorciatoia usa il percorso rapido di Clinger, una tecnica di conversione decimale che si applica quando i valori intermedi soddisfano requisiti di rappresentazione esatta. L’implementazione limita questo percorso usando condizioni numeriche esplicite. I valori al di fuori di tali condizioni proseguono attraverso un percorso di conversione più lento.
Questa distinzione è essenziale. Una scorciatoia che accettasse semplicemente errori di arrotondamento risponderebbe a una domanda ingegneristica diversa. Qui, il codice tenta di accelerare un insieme ristretto di input preservando il risultato numerico previsto.
L’implementazione raccoglie fino a 19 cifre significative. La sua scorciatoia decimale richiede inoltre un valore accumulato non superiore a 2^53 e non più di 22 cifre frazionarie. Questi limiti determinano quando il calcolo diretto è idoneo.
Il design più ampio spiega perché la revisione abbia cambiato il confronto. La validazione richiede già l’esame dei caratteri dell’input. Accumulare informazioni numeriche utili durante tale esame evita parte del lavoro che una fase di conversione separata altrimenti ripeterebbe.
Anche la gestione degli interi beneficia dello stesso approccio. Quando il valore rientra nei limiti, la funzione può restituire direttamente l’intero accumulato. Non deve costruire una stringa temporanea ripulita prima di convertire nuovamente quella stringa.
Il fallback resta un lavoro ingegneristico sostanziale. Ricostruisce testo numerico validato, usa un buffer locale quando possibile e alloca spazio di archiviazione aggiuntivo quando necessario. L’allocazione qui significa richiedere memoria temporanea per l’operazione, introducendo gestione ulteriore oltre all’aritmetica stessa.
Il codice controlla inoltre il comportamento di valutazione in virgola mobile del compilatore prima di abilitare la scorciatoia. La sua protezione affronta la possibilità che i calcoli intermedi usino una precisione diversa. Questo dettaglio mostra come una piccola ottimizzazione aritmetica possa acquisire obblighi oltre il suo apparente numero di righe.
Rust resta un confronto significativo perché i suoi candidati usano una diversa organizzazione del lavoro. L’indagine non stabilisce che una riscrittura Rust altrettanto specializzata manterrebbe gli stessi tempi. Confrontare C ottimizzato con queste versioni Rust stabilisce un risultato per quei candidati.
Un’interpretazione ragionevole è che la specializzazione abbia dato risultati per il carico di lavoro testato. La riscrittura C combina attività che prima erano separate ed evita una routine di conversione generale per i valori idonei. Questa interpretazione segue il meccanismo visibile nell’implementazione.
Spiega anche perché la reputazione di un linguaggio fornisca una previsione incompleta. Le routine generali offrono un comportamento su un ampio spazio di input; il codice specializzato può puntare a un contratto più ristretto. Le prestazioni dipendono in parte da quanto lavoro esegue il percorso scelto per gli input che arrivano effettivamente.
La questione della manutenzione segue direttamente. Chiunque possieda l’implementazione più veloce possiede anche le sue condizioni di idoneità, il comportamento del fallback e le assunzioni numeriche. Il vantaggio misurato diventa utile solo se il team può preservare queste proprietà attraverso modifiche successive.
Le dimensioni dell’estensione e la proprietà del codice tirano in direzioni diverse
L’artefatto compilato più piccolo e il percorso di manutenzione più semplice sono criteri separati, e nessuno dei due sostituisce l’altro.
Il confronto delle implementazioni del progetto riporta librerie C private delle informazioni superflue di 14.408 byte. I corrispondenti artefatti Rust misurano 320.864 byte con rusqlite e 298.464 byte con sqlite-loadable. Lo stripping rimuove determinate informazioni di build da un file compilato.
Lo stesso rapporto conta 75 righe non vuote e non di commento per il C diretto e 104 per il C ottimizzato. Rust usa 36 o 30 righe di integrazione più un parser condiviso di 41 righe. Riporta 20 dipendenze per la versione rusqlite e 62 per sqlite-loadable.
Queste misurazioni descrivono forme diverse di complessità. La lunghezza del sorgente cattura il volume visibile dell’implementazione; il conteggio delle dipendenze cattura una parte del grafo di build. Nessuno dei due misura direttamente quanto sarà difficile diagnosticare un bug futuro.
Una funzione breve può dipendere da comportamenti distribuiti tra diverse librerie. Una funzione autonoma più lunga può concentrare la responsabilità nel codice che il team dell'applicazione deve esaminare. Scegliere tra le due richiede di chiedersi dove il team desideri che tale responsabilità risieda.
L'implementazione in C rende visibili diverse responsabilità. La gestione del buffer, i controlli sull'intervallo degli interi e la scorciatoia per i decimali compaiono nel codice sorgente del progetto. I revisori possono ispezionarli direttamente, ma la visibilità non elimina la necessità di competenze specifiche.
Le funzionalità del linguaggio Rust e la relativa libreria di parsing possono ridurre alcune operazioni manuali. Ciò può migliorare la leggibilità per un team che lavora già in Rust. Tuttavia, un livello di integrazione compatto deve comunque essere esaminato per quanto riguarda la politica sugli input, il comportamento del database e la gestione degli errori.
La dimensione compilata solleva un'altra questione. Una libreria aggiuntiva ha un significato diverso in una distribuzione con vincoli rigorosi rispetto a un'applicazione che include già risorse consistenti. La differenza segnalata è misurabile; la sua importanza pratica dipende dall'ambiente di distribuzione.
Anche la dimensione del file non può rappresentare il consumo di memoria in fase di esecuzione. I byte di una libreria su disco non descrivono ogni allocazione effettuata durante l'elaborazione dei dati. Considerare il file più piccolo come l'implementazione con il minor uso di memoria richiederebbe misurazioni che questo confronto non fornisce.
La stessa cautela vale per il numero di dipendenze. Più dipendenze possono introdurre ulteriori considerazioni di build e aggiornamento, ma contarle non stabilisce i tassi di difetti. Allo stesso modo, un minor numero di dipendenze non dimostra che il codice gestito dall'applicazione sia più facile da mantenere.
Per un'applicazione C esistente, un'estensione C compatta potrebbe adattarsi alle pratiche consolidate di compilazione e revisione. Per un'applicazione Rust esistente, l'introduzione di un componente C separato potrebbe creare ulteriore lavoro di responsabilità e gestione. Si tratta di valutazioni di distribuzione, non di risultati stabiliti dal benchmark.
SQLite parse_number() presenta quindi una decisione con diverse dimensioni indipendenti. Il candidato C ottimizzato riportato è in testa nelle misurazioni del parsing e delle dimensioni compilate. Un team deve comunque decidere se mantenere il suo percorso di conversione specializzato sia compatibile con le proprie competenze e il processo di rilascio.
Un confronto utile mantiene visibili queste dimensioni. Ridurle a un unico vincitore nasconderebbe le ragioni per cui team diversi potrebbero ragionevolmente compiere scelte diverse.
Il superamento dei test non definisce il contratto dei dati
La correttezza include il significato attribuito al testo formattato, oltre al fatto che le implementazioni concordino su un risultato numerico.
Il rapporto di confronto afferma che tutte le versioni hanno superato 40 casi espliciti e 8 milioni di confronti casuali. I test casuali, spesso chiamati fuzzing, generano input per individuare comportamenti inattesi. I risultati riportati forniscono evidenze, sebbene non siano stati riprodotti in modo indipendente per questo articolo.
L'accordo tra le implementazioni è prezioso, specialmente dopo una riscrittura aritmetica. Rende meno probabile una divergenza evidente nello spazio degli input testati. Tuttavia, un accordo condiviso non può determinare se il comportamento selezionato corrisponda alle regole aziendali di una particolare applicazione.
Le percentuali illustrano la distinzione. Rimuovere un segno di percentuale conserva il numero visualizzato, mentre interpretare una percentuale come rapporto ne modifica la scala. Un'applicazione deve decidere quale significato le serve prima di considerare corretto uno dei due comportamenti.
Le indicazioni di valuta sollevano una questione correlata. Rimuovere un simbolo rende il testo più facile da elaborare numericamente, ma non stabilisce che i valori condividano la stessa unità. Combinare importi in valute diverse richiede informazioni che la sola rimozione dei caratteri non può fornire.
Anche le convenzioni di formattazione meritano attenzione. Una virgola può indicare il raggruppamento delle cifre in una convenzione e il separatore decimale in un'altra. Un parser progettato attorno a una convenzione fissa necessita che tale aspettativa sia associata ai dati ricevuti.
Si tratta di ipotetiche considerazioni di adozione, non di difetti stabiliti nell'implementazione di Willison. Spiegano perché un parser possa funzionare esattamente come progettato e tuttavia essere inadatto a un set di dati sconosciuto. La correttezza inizia dal contratto selezionato.
La precisione numerica aggiunge un altro limite. La documentazione sui numeri in virgola mobile di SQLite spiega che i suoi valori REAL usano la rappresentazione binary-64 e sono approssimativi. Molte frazioni decimali non hanno una rappresentazione esatta in quel formato, indipendentemente dal linguaggio che esegue la conversione.
Un parser correttamente arrotondato seleziona il valore rappresentabile appropriato. Non rende esatta tutta l'aritmetica successiva. Le applicazioni che richiedono calcoli decimali esatti devono valutare separatamente i propri requisiti di rappresentazione e aritmetica.
La gestione dei fallimenti merita la stessa attenzione. Restituire NULL, il marcatore di SQLite per un valore assente o sconosciuto, può consentire all'elaborazione di proseguire quando il testo non è valido. Un'applicazione dovrebbe comunque mantenere un modo per distinguere i dati rifiutati da un input numerico valido.
Altrimenti, un risultato dall'aspetto pulito può nascondere un problema di qualità dei dati. Un utente che esamina un totale deve sapere se ogni record previsto ha contribuito in modo significativo. Questa preoccupazione appartiene all'applicazione circostante, anche quando il parser rispetta il proprio contratto.
Il benchmark stesso introduce un'incertezza separata. Il tempo minimo dopo la sottrazione della baseline descrive un'esecuzione misurata favorevole. Non stabilisce le prestazioni tipiche in presenza di contesa in produzione, set di dati diversi o un ambiente informatico differente.
Nemmeno dal risultato condiviso dei test deriva un'affermazione universale sulle prestazioni. Un carico di lavoro con più input insolitamente lunghi potrebbe attivare più spesso il fallback. Ciò modificherebbe l'equilibrio del lavoro rispetto ai valori formattati comunemente testati.
Per SQLite parse_number(), una lettura scettica è quindi costruttiva. I risultati supportano l'esame di un'implementazione specifica e la riproduzione del suo comportamento. Non eliminano la necessità di definire i formati accettati, preservare i record rifiutati e verificare i risultati nell'applicazione host prevista.
Monitorare la riproduzione, le revisioni Rust e il comportamento del fallback
Le prossime evidenze utili amplierebbero il confronto mantenendo espliciti comportamento e metodi di misurazione.
Il primo segnale da osservare è la riproduzione indipendente. Un'esecuzione su un altro sistema verificherebbe quanto bene l'ordine riportato resista ai cambiamenti nell'hardware e nell'ambiente di build. Riportare sia i tempi completi delle query sia i costi di parsing derivati renderebbe queste evidenze più facili da interpretare.
Risultati coerenti rafforzerebbero la fiducia che il vantaggio del C ottimizzato si estenda oltre la configurazione originale. Un ordine diverso restringerebbe la conclusione e identificherebbe un altro fattore che merita indagine. Entrambi gli esiti migliorerebbero il confronto se fossero documentati l'ambiente e la distribuzione degli input.
La riproduzione dovrebbe inoltre preservare la distinzione tra risultati numerici e tempi. Un candidato rapido che modifica gli input accettati non risponde più alla stessa domanda. Pubblicare gli esiti di correttezza insieme alle prestazioni manterrebbe visibile questo confine.
Il secondo segnale è un'implementazione Rust rivista che affronti il lavoro specifico visibile nel confronto. Una corrispondenza più stretta nella strategia di scansione e conversione aiuterebbe a separare la struttura dell'implementazione dalla scelta del linguaggio. La domanda rilevante è se una specializzazione comparabile riduca il divario misurato.
Se Rust rivisto riducesse la differenza mantenendo i suoi vantaggi di integrazione, il risultato iniziale diventerebbe un argomento più forte per esaminare gli algoritmi prima di scegliere i linguaggi. Se il C ottimizzato mantenesse il vantaggio, gli sviluppatori otterrebbero evidenze più solide per questo particolare componente.
Rimane un confronto proposto, non un rilascio annunciato né un risultato promesso. Non vi è motivo di presumere che una versione aggiornata apparirà secondo una pianificazione fissa. Il suo valore deriverebbe dal rendere le implementazioni concorrenti più direttamente comparabili.
Il terzo segnale è costituito da evidenze sul comportamento del fallback. La scorciatoia C ottimizzata gestisce rapidamente gli input idonei, ma distribuzioni di input più ampie possono modificare la frequenza con cui viene eseguito il percorso più lento. Testare questi casi rivelerebbe di più sull'intervallo operativo pratico dell'implementazione.
Questo lavoro dovrebbe esaminare la correttezza con la stessa attenzione riservata alla velocità. Lunghe stringhe numeriche, record malformati e diverse impostazioni dell'host possono rivelare assunzioni che i valori formattati comuni non mostrano. Un risultato documentato rafforzerebbe la fiducia più di un altro tempo isolato da titolo.
Per i team di ingegneria, conservare le evidenze circostanti è importante quanto conservare il codice sorgente. Impostazioni di build, esempi di input e alternative scartate spiegano perché sia stata scelta una particolare implementazione. Conservarli in una base di conoscenza ricercabile offre ai futuri revisori un modo per riesaminare tale decisione.
Il valore immediato dell'indagine è già chiaro: un collo di bottiglia misurato ha portato a una revisione specifica e a un risultato modificato. Ciò che resta aperto è quanto ampiamente questo esito sia trasferibile. Ulteriori confronti dovrebbero ampliare le evidenze senza trasformare un esperimento mirato in un verdetto universale sui linguaggi.
Prima di adottare SQLite parse_number(), identificate la decisione che la vostra applicazione deve effettivamente affrontare. Conservate input rappresentativi, definite come dovrebbero comportarsi le percentuali e i record non validi e confrontate i candidati all'interno dell'host che li eseguirà. Mantenete i tempi completi delle query accanto a qualsiasi dato di parsing derivato, affinché un risultato favorevole a livello di componente resti collegato all'esperienza utente. Esaminate il percorso numerico specializzato con la stessa cura del relativo fallback e conservate i dettagli di build necessari per riprodurre entrambi. Il prossimo passo utile è verificare se i vantaggi di questa implementazione resistono ai vostri requisiti. Quale vincolo determina la scelta nella vostra applicazione: il tempo di conversione ripetuta, la dimensione della distribuzione o lo sforzo necessario per mantenere il parser?



