SQLite parse_number() : C dépasse Rust après une réécriture de l’analyseur
L’expérience SQLite parse_number() de Simon Willison plaçait initialement Rust devant C, jusqu’à ce qu’une réécriture ciblée de l’analyseur inverse le classement des performances. Publiée le 8 novembre 2026, l’enquête compare des extensions, des modules compilés qui ajoutent des fonctions de base de données, destinées à nettoyer des nombres formatés. L’implémentation C obtenue aurait dominé chaque jeu de données testé tout en conservant un fichier compilé plus petit.
La tâche paraît modeste : convertir un texte contenant des symboles monétaires, des virgules ou des signes de pourcentage en valeurs adaptées au tri numérique et à l’addition. Pourtant, la comparaison a révélé des réponses différentes selon les questions d’ingénierie posées. Rust a fourni le premier analyseur décimal le plus rapide ; C optimisé a finalement fourni l’implémentation mesurée la plus rapide.
Pour les développeurs qui évaluent du code généré par IA, l’enjeu se situe dans le processus de revue. Une implémentation plausible et un langage familier apportent peu d’indices sur l’origine du temps d’exécution. L’enquête de Willison est devenue utile lorsque les mesures ont identifié une routine de conversion précise et suscité une expérimentation plus ciblée.
SQLite parse_number() modifie le résultat après un second passage
Le vainqueur annoncé a changé lorsque l’implémentation a changé, plaçant la conception de l’analyseur au cœur de la comparaison.
Willison a commencé par demander à Claude, identifié dans son compte comme Opus 5.5, d’explorer la fonction en C et en Rust. La comparaison demandée couvrait la vitesse d’exécution, la complexité du code et la taille de la bibliothèque obtenue. Il décrit l’enquête dans son expérience d’analyse SQLite.
Les premiers résultats ont séparé la taille du fichier de la vitesse. Willison a indiqué une extension C d’environ 14 KB, contre 321 KB avec rusqlite et 298 KB avec sqlite-loadable-rs. Ces bibliothèques Rust relient du code Rust à SQLite. Malgré son fichier plus petit, l’implémentation C initiale était moins performante sur les entrées mixtes et décimales.
L’explication s’est concentrée sur strtod(), une fonction de bibliothèque C qui convertit du texte en nombre à virgule flottante, une représentation numérique approximative. Willison a attribué l’avantage initial de Rust sur les décimales à sa routine de conversion plus rapide. Il a ensuite demandé à Claude de produire un analyseur C égalant ou dépassant l’implémentation Rust.
La version C révisée a dominé la comparaison rapportée. Sur les données mixtes, les coûts nets arrondis sont passés de 67 nanosecondes par ligne pour le C initial à 19 pour le C révisé. Les versions Rust ont enregistré 46 et 41. Les entrées composées uniquement de décimales ont montré une évolution encore plus forte au sein de C, de 106 à 18 nanosecondes par ligne.
Cette séquence donne à l’histoire son importance. S’arrêter après la première comparaison aurait soutenu le choix de Rust pour la charge décimale mesurée. Poursuivre avec une révision ciblée a produit un autre candidat, sans modifier la fonction de base de données demandée.
L’exercice distingue également la génération d’alternatives de leur évaluation. Claude a fourni les implémentations, mais leur valeur relative s’est révélée à travers les mesures environnantes. La réponse initiale offrait un point de départ plutôt qu’une décision d’ingénierie définitive.
Rien ne permet ici de traiter ce résultat comme un classement universel des langages de programmation. Il s’agit d’implémentations particulières d’une petite fonction. Leurs différences incluent les routines de conversion, le stockage temporaire et le code d’intégration, qui se situent tous sous un titre général opposant C à Rust.
Le changement pratique est plus précis : les développeurs disposent d’un exemple publié où une première ébauche C lente est devenue le candidat mesuré le plus rapide. Cela rend ce cas utile pour examiner les choix d’implémentation, tout en laissant les affirmations plus générales sur les langages sans fondement.
Le benchmark mesure des analyses répétées dans SQLite
La charge de travail donne un rôle central à la conversion répétée ; ses résultats doivent donc être interprétés dans ce contexte.
Le harness de benchmark publié crée une base de données en mémoire contenant 2 millions de lignes. Il évalue une requête qui analyse chaque valeur et additionne les résultats. Python lance la requête, tandis que SQLite effectue le travail répétitif dans la base de données.
Cette configuration importe, car elle évite de placer une boucle Python autour de chaque conversion individuelle. L’expérience cible donc plus directement l’appel combiné de fonction de base de données et le chemin d’analyse. Elle ne mesure pas principalement une application Python franchissant à répétition la frontière vers du code natif.
Le harness alterne les candidats sur 15 exécutions. Il enregistre les temps écoulés minimum et médian, puis calcule le chiffre net annoncé à partir du minimum. Un balayage de référence qui additionne les longueurs des chaînes est soustrait avant que le résultat ne devienne des nanosecondes par ligne.
Cette soustraction aide à isoler le travail incrémental, mais le chiffre reste une estimation comparative. Il ne correspond pas à la durée complète d’une importation, d’un rafraîchissement de tableau de bord ou d’une requête impliquant d’autres opérations. L’interpréter comme un temps de réponse visible par l’utilisateur étendrait la mesure au-delà de sa conception.
Les jeux de données façonnent également le résultat. Des exécutions distinctes couvrent du texte entier, du texte décimal et des valeurs formatées mixtes. Le générateur mixte comprend des valeurs avec symbole monétaire, des pourcentages, des chaînes numériques ordinaires et des entrées invalides. Ces variations permettent de distinguer un problème de conversion décimale d’un problème d’analyse plus général.
Prenons une application qui trie à répétition des rapports financiers importés. Si l’analyse a lieu lors de chaque requête, le travail de conversion devient une composante de chaque demande de rapport. Si l’application normalise plutôt les valeurs lors de l’importation, le même analyseur affecte une autre étape et répond à une exigence de performance différente.
Aucune de ces architectures ne découle automatiquement de ces mesures. L’expérience apporte des éléments sur le composant d’analyse, tandis que l’application détermine la fréquence d’exécution de ce composant. Une fonction isolée plus rapide compte surtout lorsqu’elle représente une part significative de la charge réelle.
L’alternative SQL pure du benchmark utilise des appels replace() imbriqués, c’est-à-dire des substitutions de chaînes répétées, suivies d’une conversion numérique. Sa présence offre aux développeurs un point de comparaison familier. Toutefois, remplacer des caractères et appliquer une politique complète de traitement des entrées sont des tâches différentes ; l’équivalence de comportement exige donc un examen distinct.
La pression repose donc sur les développeurs qui utilisent de courts résumés de benchmarks pour justifier leurs choix d’implémentation. Avant d’adopter le classement rapporté, ils doivent identifier à quel moment l’analyse intervient dans leur application. Ils doivent également déterminer si leurs entrées ressemblent aux jeux de données générés.
Pour SQLite parse_number(), le harness rend ces questions vérifiables. Les lecteurs peuvent examiner à la fois la requête chronométrée et le générateur de données, plutôt que de se fier uniquement à un chiffre gagnant.
C gagne en vitesse en combinant validation et conversion
L’implémentation C optimisée réduit le travail intermédiaire tout en conservant une solution de repli pour les entrées hors des limites de son raccourci.
L’analyseur décimal optimisé examine les caractères tout en accumulant des informations numériques. Durant cette analyse, il suit les chiffres significatifs et compte les chiffres après la virgule décimale. Cela permet aux entrées appropriées d’éviter une reconstruction sous forme de chaîne nettoyée.
Son raccourci utilise le chemin rapide de Clinger, une technique de conversion décimale applicable lorsque les valeurs intermédiaires satisfont aux exigences de représentation exacte. L’implémentation restreint ce chemin au moyen de conditions numériques explicites. Les valeurs hors de ces conditions passent par une voie de conversion plus lente.
Cette distinction est essentielle. Un raccourci qui accepterait simplement des erreurs d’arrondi répondrait à une question d’ingénierie différente. Ici, le code cherche à accélérer un ensemble restreint d’entrées tout en préservant le résultat numérique visé.
L’implémentation collecte jusqu’à 19 chiffres significatifs. Son raccourci décimal exige en outre une valeur accumulée ne dépassant pas 2^53 et pas plus de 22 chiffres fractionnaires. Ces limites déterminent l’éligibilité au calcul direct.
La conception plus large explique pourquoi la révision a modifié la comparaison. La validation exige déjà l’examen des caractères de l’entrée. Accumuler des informations numériques utiles pendant cet examen évite une partie du travail qu’une étape de conversion distincte répéterait autrement.
Le traitement des entiers bénéficie de la même approche. Lorsque la valeur convient, la fonction peut renvoyer directement l’entier accumulé. Elle n’a pas besoin de construire une chaîne nettoyée temporaire avant de convertir à nouveau cette chaîne.
La solution de repli demeure un travail d’ingénierie substantiel. Elle reconstruit un texte numérique validé, utilise un tampon local lorsque cela est possible et alloue du stockage supplémentaire lorsque nécessaire. L’allocation désigne ici la demande de mémoire temporaire pour l’opération, ce qui introduit une gestion allant au-delà de l’arithmétique elle-même.
Le code vérifie également le comportement d’évaluation en virgule flottante du compilateur avant d’activer le raccourci. Sa protection traite la possibilité que les calculs intermédiaires utilisent une précision différente. Ce détail montre comment une petite optimisation arithmétique peut entraîner des obligations dépassant son nombre apparent de lignes.
Rust reste une comparaison significative, car ses candidats utilisent une organisation différente du travail. L’enquête n’établit pas qu’une réécriture Rust tout aussi spécialisée conserverait le même temps d’exécution. Comparer le C optimisé à ces versions Rust établit un résultat pour ces candidats.
Une interprétation raisonnable est que la spécialisation a porté ses fruits pour la charge testée. La réécriture C combine des tâches auparavant séparées et évite une routine de conversion générale pour les valeurs éligibles. Cette interprétation découle du mécanisme visible dans l’implémentation.
Elle explique aussi pourquoi la réputation d’un langage fournit une prédiction incomplète. Les routines générales offrent un comportement sur un vaste espace d’entrées ; le code spécialisé peut cibler un contrat plus étroit. Les performances dépendent en partie de la quantité de travail effectuée par le chemin choisi pour les entrées réellement reçues.
La question de la maintenance en découle directement. Quiconque possède l’implémentation plus rapide possède également ses conditions d’éligibilité, son comportement de repli et ses hypothèses numériques. L’avantage mesuré ne devient utile que si l’équipe peut préserver ces propriétés lors de modifications ultérieures.
La taille de l’extension et la propriété du code tirent dans des directions différentes
L’artefact compilé plus petit et le chemin de maintenance plus simple sont des critères distincts, et aucun ne remplace l’autre.
La comparaison des implémentations du projet indique des bibliothèques C dépouillées de 14 408 octets. Les artefacts Rust correspondants mesurent 320 864 octets avec rusqlite et 298 464 octets avec sqlite-loadable. Le dépouillement retire certaines informations de compilation d’un fichier compilé.
Le même rapport compte 75 lignes non vides et non commentées pour le C direct, et 104 pour le C optimisé. Rust utilise 36 ou 30 lignes d’intégration, plus un analyseur partagé de 41 lignes. Il indique 20 dépendances pour la version rusqlite et 62 pour sqlite-loadable.
Ces mesures décrivent différentes formes de complexité. La longueur du code source capture le volume visible de l’implémentation ; le nombre de dépendances capture une partie du graphe de compilation. Aucune ne mesure directement la difficulté à diagnostiquer un futur bug.
Une fonction courte peut dépendre d’un comportement réparti entre plusieurs bibliothèques. Une fonction autonome plus longue peut concentrer la responsabilité dans un code que l’équipe applicative doit examiner. Choisir entre les deux suppose de déterminer où l’équipe souhaite que cette responsabilité réside.
L’implémentation en C rend plusieurs responsabilités visibles. Sa gestion des tampons, ses vérifications de plage d’entiers et son raccourci décimal apparaissent dans le code source du projet lui-même. Les réviseurs peuvent les examiner directement, mais cette visibilité ne supprime pas la nécessité d’une expertise.
Les fonctionnalités du langage Rust et sa bibliothèque d’analyse peuvent réduire certaines manipulations manuelles. Cela peut améliorer la lisibilité pour une équipe travaillant déjà en Rust. Toutefois, une couche d’intégration compacte doit toujours être examinée en matière de politique d’entrée, de comportement de la base de données et de gestion des erreurs.
La taille du binaire compilé soulève une autre question. Une bibliothèque supplémentaire n’a pas la même importance dans une distribution fortement contrainte que dans une application contenant déjà des ressources substantielles. La différence rapportée est mesurable ; son importance pratique dépend de l’environnement de livraison.
La taille de fichier ne peut pas non plus servir d’indicateur de la consommation mémoire à l’exécution. Les octets d’une bibliothèque sur le disque ne décrivent pas toutes les allocations effectuées pendant le traitement des données. Considérer le plus petit fichier comme l’implémentation la moins gourmande en mémoire nécessiterait des mesures que cette comparaison ne fournit pas.
La même prudence s’applique au nombre de dépendances. Davantage de dépendances peuvent entraîner davantage de considérations de compilation et de mise à jour, mais les compter n’établit pas les taux de défauts. De même, moins de dépendances ne démontre pas que le code détenu par l’application est plus facile à maintenir.
Pour une application C existante, une extension C compacte peut s’intégrer aux pratiques établies de compilation et de revue. Pour une application Rust existante, l’introduction d’un composant C séparé peut créer un travail supplémentaire de responsabilité. Il s’agit de jugements de déploiement, plutôt que de résultats établis par le benchmark.
SQLite parse_number() présente donc une décision comportant plusieurs dimensions indépendantes. Le candidat C optimisé rapporté est en tête pour l’analyse mesurée et la taille compilée. Une équipe doit néanmoins déterminer si la maintenance de son chemin de conversion spécialisé correspond à ses compétences et à son processus de publication.
Une comparaison utile maintient ces dimensions visibles. Les réduire à un seul gagnant masquerait les raisons pour lesquelles différentes équipes pourraient raisonnablement faire des choix différents.
La réussite des tests ne tranche pas le contrat de données
La correction inclut le sens attribué au texte formaté, au-delà de la question de savoir si les implémentations s’accordent sur une réponse numérique.
Le rapport de comparaison indique que toutes les versions ont réussi 40 cas explicites et 8 millions de comparaisons aléatoires. Les tests aléatoires, souvent appelés fuzzing, génèrent des entrées afin de rechercher des comportements inattendus. Ces résultats rapportés constituent des éléments probants, bien qu’ils n’aient pas été reproduits indépendamment pour cet article.
L’accord entre les implémentations est précieux, particulièrement après une réécriture arithmétique. Il rend une divergence manifeste moins probable dans l’espace d’entrée testé. Pourtant, un accord partagé ne peut pas déterminer si le comportement retenu correspond aux règles métier d’une application donnée.
Les pourcentages illustrent cette distinction. Supprimer un signe de pourcentage préserve le nombre affiché, tandis qu’interpréter un pourcentage comme un ratio modifie son échelle. Une application doit décider du sens dont elle a besoin avant de considérer l’un ou l’autre comportement comme correct.
Les marquages monétaires soulèvent une question connexe. Supprimer un symbole rend le texte plus facile à traiter numériquement, mais n’établit pas que les valeurs partagent la même unité. Combiner des montants dans différentes devises exige des informations que la seule suppression de caractères ne peut pas fournir.
Les conventions de formatage méritent également de l’attention. Une virgule peut indiquer le regroupement des chiffres dans une convention et une séparation décimale dans une autre. Un analyseur conçu autour d’une convention fixe doit associer cette attente aux données qu’il reçoit.
Il s’agit de préoccupations hypothétiques liées à l’adoption, et non de défauts établis dans l’implémentation de Willison. Elles expliquent pourquoi un analyseur peut fonctionner exactement comme prévu tout en étant inadapté à un jeu de données inconnu. La correction commence avec le contrat sélectionné.
La précision numérique ajoute une autre limite. La documentation sur les nombres à virgule flottante de SQLite explique que ses valeurs REAL utilisent une représentation binaire-64 et sont approximatives. De nombreuses fractions décimales n’ont pas de représentation exacte dans ce format, indépendamment du langage qui effectue la conversion.
Un analyseur correctement arrondi sélectionne la valeur représentable appropriée. Il ne rend pas toutes les opérations arithmétiques ultérieures exactes. Les applications nécessitant des calculs décimaux exacts doivent évaluer séparément leurs exigences de représentation et d’arithmétique.
La gestion des échecs mérite la même attention. Renvoyer NULL, le marqueur de SQLite pour une valeur absente ou inconnue, peut permettre au traitement de se poursuivre lorsque le texte est invalide. Une application devrait néanmoins conserver un moyen de distinguer les données rejetées d’une entrée numérique valide.
Sinon, un résultat d’apparence propre peut masquer un problème de qualité des données. Un utilisateur qui examine un total doit savoir si chaque enregistrement attendu a contribué de façon significative. Cette préoccupation relève de l’application environnante, même lorsque l’analyseur respecte son contrat.
Le benchmark lui-même introduit une incertitude distincte. Le temps minimal après soustraction de la ligne de base décrit une exécution mesurée favorable. Il n’établit pas les performances typiques sous contention en production, avec des jeux de données variés ou dans un environnement informatique différent.
Aucune affirmation universelle sur les performances ne découle non plus du résultat de test partagé. Une charge de travail contenant davantage d’entrées inhabituellement longues pourrait solliciter plus souvent le mécanisme de repli. Cela modifierait l’équilibre du travail par rapport aux valeurs couramment formatées testées.
Pour SQLite parse_number(), une lecture sceptique est donc constructive. Les résultats justifient l’examen d’une implémentation précise et la reproduction de son comportement. Ils ne suppriment pas la nécessité de définir les formats acceptés, de conserver les enregistrements rejetés et de vérifier les résultats dans l’application hôte prévue.
Surveiller la reproduction, les révisions Rust et le comportement de repli
Les prochains éléments probants utiles élargiraient la comparaison tout en maintenant explicites le comportement et les méthodes de mesure.
Le premier signal à surveiller est une reproduction indépendante. Une exécution sur un autre système testerait dans quelle mesure l’ordre rapporté résiste aux changements de matériel et d’environnement de compilation. La publication à la fois des temps de requête complets et des coûts d’analyse dérivés rendrait ces éléments plus faciles à interpréter.
Des résultats concordants renforceraient la confiance dans le fait que l’avantage du C optimisé s’étend au-delà de la configuration d’origine. Un ordre différent restreindrait la conclusion et identifierait un autre facteur à étudier. Dans les deux cas, la comparaison serait améliorée si l’environnement et la distribution des entrées étaient documentés.
La reproduction devrait également préserver la distinction entre résultats numériques et chronométrage. Un candidat rapide qui modifie les entrées acceptées ne répond plus à la même question. Publier les résultats de correction parallèlement aux performances maintiendrait cette limite visible.
Le deuxième signal est une implémentation Rust révisée traitant le travail précis visible dans la comparaison. Une correspondance plus étroite dans la stratégie de balayage et de conversion aiderait à distinguer la structure de l’implémentation du choix du langage. La question pertinente est de savoir si une spécialisation comparable réduit l’écart mesuré.
Si une version Rust révisée réduit la différence tout en conservant ses avantages d’intégration, le résultat initial devient un argument plus solide en faveur de l’examen des algorithmes avant la sélection des langages. Si le C optimisé conserve son avance, les développeurs disposeront d’éléments plus solides pour ce composant particulier.
Cela reste une comparaison proposée, et non une version annoncée ou un résultat promis. Rien ne permet de supposer qu’une version mise à jour paraîtra selon un calendrier fixe. Sa valeur viendrait d’une comparaison plus directe entre les implémentations concurrentes.
Le troisième signal concerne les éléments probants sur le comportement de repli. Le raccourci C optimisé traite rapidement les entrées éligibles, mais des distributions d’entrée plus larges peuvent modifier la fréquence d’exécution de la voie plus lente. Tester ces cas révélerait davantage sur la plage de fonctionnement pratique de l’implémentation.
Ce travail devrait examiner la correction avec autant d’attention que la vitesse. De longues chaînes numériques, des enregistrements malformés et différents réglages de l’hôte peuvent révéler des hypothèses que les valeurs couramment formatées ne révèlent pas. Un résultat documenté renforcerait davantage la confiance qu’un nouveau chronométrage isolé.
Pour les équipes d’ingénierie, conserver les éléments environnants compte autant que conserver le code source. Les paramètres de compilation, les exemples d’entrée et les alternatives rejetées expliquent pourquoi une implémentation particulière a été choisie. Les conserver dans une base de connaissances consultable offre aux futurs réviseurs un moyen de réexaminer cette décision.
La valeur immédiate de l’enquête est déjà claire : un goulot d’étranglement mesuré a conduit à une révision précise et à un résultat modifié. Ce qui demeure ouvert est la portée de ce résultat. Les comparaisons ultérieures devraient élargir les éléments probants sans transformer une expérience ciblée en verdict universel sur les langages.
Avant d’adopter SQLite parse_number(), identifiez la décision à laquelle votre application est réellement confrontée. Conservez des entrées représentatives, définissez le comportement attendu pour les pourcentages et les enregistrements invalides, puis comparez les candidats dans l’hôte qui les exécutera. Conservez les temps de requête complets à côté de toute mesure d’analyse dérivée, afin qu’un résultat favorable pour un composant reste lié à l’expérience utilisateur. Examinez le chemin numérique spécialisé avec le même soin que son mécanisme de repli, et conservez les détails de compilation nécessaires à la reproduction des deux. L’étape utile suivante consiste à vérifier si les avantages de cette implémentation résistent à vos exigences. Quelle contrainte détermine le choix dans votre application : le temps de conversion répété, la taille de distribution ou l’effort nécessaire pour maintenir l’analyseur ?



