SQLite parse_number(): C supera a Rust tras reescribir un parser
El experimento de Simon Willison con SQLite parse_number() situó inicialmente a Rust por delante de C, hasta que una reescritura específica del parser invirtió la clasificación de rendimiento. Publicada el 8 de noviembre de 2026, la investigación compara extensiones —complementos compilados que incorporan funciones de base de datos— para limpiar números con formato. Según se informa, la implementación resultante en C lideró todos los conjuntos de datos evaluados y mantuvo un archivo compilado más pequeño.
La tarea parece modesta: convertir texto con símbolos monetarios, comas o signos de porcentaje en valores aptos para ordenación numérica y sumas. Sin embargo, la comparación reveló respuestas distintas a diferentes preguntas de ingeniería. Rust ofreció el parser decimal inicial más rápido; C optimizado acabó proporcionando la implementación medida más rápida.
Para los desarrolladores que evalúan código generado por IA, la presión recae en el proceso de revisión. Una implementación plausible y un lenguaje conocido aportan poca evidencia sobre dónde se consume el tiempo de ejecución. La investigación de Willison se volvió útil cuando la medición identificó una rutina de conversión concreta e impulsó un experimento más acotado.
SQLite parse_number() cambia el resultado tras una segunda pasada
El ganador informado cambió al modificarse la implementación, lo que convierte el diseño del parser en el eje de la comparación.
Willison comenzó pidiendo a Claude, identificado en su cuenta como Opus 5.5, que explorara la función en C y Rust. La comparación solicitada abarcaba velocidad de ejecución, complejidad del código y el tamaño de la biblioteca resultante. Describe la investigación en su experimento de análisis de SQLite.
Los primeros resultados separaron el tamaño de archivo de la velocidad. Willison informó de una extensión en C de aproximadamente 14 KB, frente a 321 KB con rusqlite y 298 KB con sqlite-loadable-rs. Estas bibliotecas de Rust conectan código Rust con SQLite. Pese a su archivo más pequeño, la implementación inicial en C perdió en entradas mixtas y decimales.
La explicación se centró en strtod(), una función de la biblioteca C que convierte texto en un número de punto flotante, una representación numérica aproximada. Willison atribuyó la ventaja decimal inicial de Rust a su rutina de conversión más rápida. Luego pidió a Claude que produjera un parser en C que igualara o superara la implementación de Rust.
La versión revisada en C lideró la comparación informada. En datos mixtos, los costes netos redondeados bajaron de 67 nanosegundos por fila para el C inicial a 19 para el C revisado. Las versiones de Rust registraron 46 y 41. Las entradas solo decimales mostraron un cambio aún mayor dentro de C, de 106 a 18 nanosegundos por fila.
Esa secuencia da relevancia a la historia. Detenerse tras la primera comparación respaldaría elegir Rust para la carga de trabajo decimal medida. Continuar con una revisión específica produjo un candidato distinto, sin modificar la función de base de datos solicitada.
El ejercicio también separa la generación de alternativas de su evaluación. Claude proporcionó implementaciones, pero su valor relativo surgió mediante las mediciones circundantes. La respuesta inicial aportó un punto de partida, no una decisión de ingeniería definitiva.
No hay base aquí para tratar el resultado como una clasificación universal de lenguajes de programación. Se trata de implementaciones concretas de una función pequeña. Sus diferencias incluyen rutinas de conversión, almacenamiento temporal y código de integración, todo ello bajo un titular amplio de C frente a Rust.
El cambio práctico es más específico: los desarrolladores tienen un ejemplo publicado en el que un primer borrador lento en C se convirtió en el candidato medido más rápido. Esto hace que el caso sea útil para examinar decisiones de implementación, sin respaldar afirmaciones más amplias sobre lenguajes.
El benchmark mide análisis repetido dentro de SQLite
La carga de trabajo otorga un papel central a la conversión repetida, por lo que sus resultados necesitan ese contexto.
El arnés de benchmark publicado crea una base de datos en memoria con 2 millones de filas. Evalúa una consulta que analiza cada valor y suma los resultados. Python inicia la consulta, mientras SQLite realiza el trabajo repetido en la base de datos.
Esta disposición importa porque evita colocar un bucle de Python alrededor de cada conversión individual. Por tanto, el experimento se dirige de forma más directa a la combinación de la llamada a la función de base de datos y la ruta de análisis. No mide principalmente una aplicación Python que cruza repetidamente hacia código nativo.
El arnés alterna candidatos a lo largo de 15 ejecuciones. Registra los tiempos transcurridos mínimo y mediano, y después calcula la cifra neta anunciada a partir del mínimo. Antes de expresar el resultado en nanosegundos por fila, se resta un escaneo de referencia que suma las longitudes de las cadenas.
Esa sustracción ayuda a aislar el trabajo incremental, pero la cifra sigue siendo una estimación comparativa. No representa la duración completa de una importación, una actualización de panel o una consulta con otras operaciones. Interpretarla como tiempo de respuesta para el usuario extendería la medición más allá de su diseño.
Los conjuntos de datos también moldean el resultado. Las ejecuciones separadas cubren texto entero, texto decimal y valores mixtos con formato. El generador mixto incluye valores con marcas monetarias, porcentajes, cadenas numéricas normales y entradas no válidas. Estas variaciones permiten distinguir un problema de conversión decimal de un problema general de análisis.
Pensemos en una aplicación que ordena repetidamente informes financieros importados. Si el análisis ocurre en cada consulta, el trabajo de conversión forma parte de cada solicitud de informe. Si la aplicación normaliza los valores durante la importación, el mismo parser afecta a otra etapa y afronta un requisito de rendimiento diferente.
Ninguna de las dos arquitecturas se deriva automáticamente de estas mediciones. El experimento aporta evidencia sobre el componente de análisis, mientras la aplicación determina con qué frecuencia se ejecuta ese componente. Una función aislada más rápida importa más cuando ocupa una proporción significativa de la carga de trabajo real.
La alternativa puramente SQL del benchmark utiliza llamadas replace() anidadas, es decir, sustituciones repetidas de cadenas, seguidas de conversión numérica. Su presencia ofrece a los desarrolladores un punto de comparación familiar. Sin embargo, reemplazar caracteres y imponer una política completa de entrada son tareas distintas, por lo que un comportamiento equivalente requiere un examen independiente.
La presión recae, por tanto, en los desarrolladores que usan resúmenes breves de benchmarks para justificar decisiones de implementación. Antes de adoptar la clasificación informada, deben identificar dónde ocurre el análisis en su aplicación. También deben determinar si sus entradas se parecen a los conjuntos de datos generados.
Para SQLite parse_number(), el arnés hace examinables esas preguntas. Los lectores pueden revisar tanto la consulta cronometrada como el generador de datos, en lugar de depender únicamente de una cifra ganadora.
C gana velocidad al combinar validación y conversión
La implementación optimizada en C reduce el trabajo intermedio y conserva una alternativa para las entradas fuera de los límites de su atajo.
El parser decimal optimizado inspecciona los caracteres mientras acumula información numérica. Durante ese recorrido, rastrea los dígitos significativos y cuenta los dígitos posteriores al separador decimal. Esto permite que las entradas adecuadas eviten reconstruirse como una cadena limpia.
Su atajo utiliza la ruta rápida de Clinger, una técnica de conversión decimal que se aplica cuando los valores intermedios cumplen requisitos de representación exacta. La implementación restringe esta ruta mediante condiciones numéricas explícitas. Los valores fuera de esas condiciones continúan por una ruta de conversión más lenta.
Esa distinción es esencial. Un atajo que simplemente aceptara errores de redondeo respondería a una pregunta de ingeniería diferente. Aquí, el código intenta acelerar un conjunto restringido de entradas conservando el resultado numérico previsto.
La implementación recopila hasta 19 dígitos significativos. Su atajo decimal exige además un valor acumulado no superior a 2^53 y no más de 22 dígitos fraccionarios. Estos límites determinan cuándo el cálculo directo es apto.
El diseño más amplio explica por qué la revisión cambió la comparación. La validación ya exige examinar los caracteres de entrada. Acumular información numérica útil durante ese examen evita parte del trabajo que, de otro modo, repetiría una etapa de conversión separada.
El manejo de enteros se beneficia del mismo enfoque. Cuando el valor cabe, la función puede devolver directamente el entero acumulado. No necesita construir una cadena limpia temporal antes de volver a convertir esa cadena.
La alternativa sigue suponiendo un trabajo de ingeniería considerable. Reconstruye texto numérico validado, utiliza un búfer local cuando es posible y asigna almacenamiento adicional cuando resulta necesario. La asignación aquí significa solicitar memoria temporal para la operación, lo que añade procesamiento más allá de la aritmética en sí.
El código también comprueba el comportamiento de evaluación de punto flotante del compilador antes de habilitar el atajo. Su protección aborda la posibilidad de que los cálculos intermedios utilicen una precisión distinta. Este detalle muestra cómo una pequeña optimización aritmética puede adquirir obligaciones más allá de su aparente número de líneas.
Rust sigue siendo una comparación relevante porque sus candidatos emplean una organización del trabajo diferente. La investigación no establece que una reescritura de Rust igualmente especializada conservaría el mismo tiempo. Comparar C optimizado con estas versiones de Rust establece un resultado para esos candidatos.
Una interpretación razonable es que la especialización compensó para la carga de trabajo evaluada. La reescritura en C combina tareas que antes estaban separadas y evita una rutina de conversión general para los valores aptos. Esa interpretación se deriva del mecanismo visible en la implementación.
También explica por qué la reputación de un lenguaje ofrece una predicción incompleta. Las rutinas generales ofrecen comportamiento en un espacio amplio de entradas; el código especializado puede dirigirse a un contrato más estrecho. El rendimiento depende en parte de cuánto trabajo realiza la ruta elegida para las entradas que realmente llegan.
La cuestión del mantenimiento surge directamente. Quien sea responsable de la implementación más rápida también asume sus condiciones de elegibilidad, comportamiento de alternativa y supuestos numéricos. La ventaja medida solo resulta útil si el equipo puede preservar esas propiedades en cambios posteriores.
El tamaño de la extensión y la propiedad del código tiran en direcciones distintas
El artefacto compilado más pequeño y la ruta de mantenimiento más sencilla son criterios independientes, y ninguno sustituye al otro.
La comparación de implementaciones del proyecto informa de bibliotecas C reducidas de 14.408 bytes. Los artefactos Rust correspondientes miden 320.864 bytes con rusqlite y 298.464 bytes con sqlite-loadable. La reducción elimina determinada información de compilación de un archivo compilado.
El mismo informe cuenta 75 líneas no vacías ni comentadas para C directo y 104 para C optimizado. Rust utiliza 36 o 30 líneas de integración más un parser compartido de 41 líneas. Informa de 20 dependencias para la versión rusqlite y 62 para sqlite-loadable.
Estas mediciones describen formas distintas de complejidad. La longitud del código fuente captura el volumen visible de implementación; el número de dependencias captura una parte del grafo de compilación. Ninguna mide directamente lo difícil que será diagnosticar un futuro error.
Una función corta puede depender de comportamientos distribuidos entre varias bibliotecas. Una función autónoma más extensa puede concentrar la responsabilidad en código que el equipo de la aplicación debe revisar. Elegir entre ambas exige decidir dónde quiere el equipo que resida esa responsabilidad.
La implementación en C hace visibles varias responsabilidades. Su manejo de búferes, las comprobaciones de rango de enteros y el atajo decimal aparecen en el propio código fuente del proyecto. Los revisores pueden inspeccionarlos directamente, pero la visibilidad no elimina la necesidad de experiencia especializada.
Las capacidades del lenguaje Rust y su biblioteca de análisis pueden reducir parte del manejo manual. Eso puede mejorar la legibilidad para un equipo que ya trabaja con Rust. Sin embargo, una capa de integración compacta sigue necesitando revisión en cuanto a políticas de entrada, comportamiento de la base de datos y manejo de errores.
El tamaño compilado plantea otra cuestión. Una biblioteca adicional tiene una importancia distinta en una distribución con restricciones estrictas que en una aplicación que ya incluye recursos considerables. La diferencia reportada es medible; su importancia práctica depende del entorno de entrega.
El tamaño del archivo tampoco puede representar el consumo de memoria en tiempo de ejecución. Los bytes de una biblioteca en disco no describen todas las asignaciones realizadas al procesar datos. Considerar el archivo más pequeño como la implementación que menos memoria consume requeriría mediciones que esta comparación no aporta.
La misma cautela se aplica al número de dependencias. Más dependencias pueden introducir más consideraciones de compilación y actualización, pero contarlas no establece tasas de defectos. Del mismo modo, menos dependencias no demuestran que el código propiedad de la aplicación sea más fácil de mantener.
Para una aplicación existente en C, una extensión compacta en C podría encajar con las prácticas de compilación y revisión ya establecidas. Para una aplicación existente en Rust, introducir un componente independiente en C podría crear trabajo adicional de mantenimiento. Son juicios de despliegue, no resultados establecidos por el benchmark.
Por tanto, SQLite parse_number() presenta una decisión con varias dimensiones independientes. El candidato optimizado en C lidera en análisis medido y tamaño compilado. Aun así, un equipo debe decidir si mantener su ruta de conversión especializada encaja con sus competencias y su proceso de lanzamiento.
La comparación útil mantiene visibles esas dimensiones. Reducirlas a un único ganador ocultaría las razones por las que distintos equipos podrían tomar decisiones diferentes de forma razonable.
Superar las pruebas no resuelve el contrato de datos
La corrección incluye el significado asignado al texto con formato, más allá de que las implementaciones coincidan en una respuesta numérica.
El informe de comparación afirma que todas las versiones superaron 40 casos explícitos y 8 millones de comparaciones aleatorias. Las pruebas aleatorias, a menudo llamadas fuzzing, generan entradas para buscar comportamientos inesperados. Esos resultados reportados aportan evidencia, aunque no se han reproducido de forma independiente para este artículo.
La concordancia entre implementaciones es valiosa, especialmente después de una reescritura aritmética. Hace menos probable una divergencia evidente dentro del espacio de entradas probado. Sin embargo, el acuerdo compartido no puede determinar si el comportamiento elegido se ajusta a las reglas de negocio de una aplicación concreta.
Los porcentajes ilustran la diferencia. Eliminar un signo de porcentaje conserva el número mostrado, mientras que interpretar un porcentaje como una proporción cambia su escala. Una aplicación debe decidir qué significado necesita antes de considerar correcto cualquiera de los dos comportamientos.
Las marcas monetarias plantean una cuestión relacionada. Eliminar un símbolo facilita el procesamiento numérico del texto, pero no establece que los valores compartan la misma unidad. Combinar importes de distintas monedas requiere información que la simple eliminación de caracteres no puede aportar.
Las convenciones de formato también merecen atención. Una coma puede indicar agrupación de dígitos en una convención y un separador decimal en otra. Un analizador diseñado en torno a una convención fija necesita que esa expectativa esté asociada a los datos que recibe.
Estas son preocupaciones hipotéticas de adopción, no defectos establecidos en la implementación de Willison. Explican por qué un analizador puede funcionar exactamente como fue diseñado y aun así resultar inadecuado para un conjunto de datos desconocido. La corrección comienza con el contrato seleccionado.
La precisión numérica añade otro límite. La documentación de punto flotante de SQLite explica que sus valores REAL usan representación binaria de 64 bits y son aproximados. Muchas fracciones decimales carecen de una representación exacta en ese formato, independientemente del lenguaje que realice la conversión.
Un analizador correctamente redondeado selecciona el valor representable adecuado. No hace exacta toda la aritmética posterior. Las aplicaciones que requieren cálculos decimales exactos deben evaluar por separado sus requisitos de representación y aritmética.
El manejo de fallos merece la misma atención. Devolver NULL, el marcador de SQLite para un valor ausente o desconocido, puede permitir que el procesamiento continúe cuando el texto no es válido. Una aplicación debería conservar, aun así, una forma de distinguir los datos rechazados de las entradas numéricas válidas.
De lo contrario, un resultado de apariencia limpia puede ocultar un problema de calidad de datos. Un usuario que examine un total necesita saber si cada registro esperado contribuyó de forma significativa. Esa preocupación corresponde a la aplicación circundante, incluso cuando el analizador cumple su contrato.
El benchmark en sí introduce una incertidumbre aparte. El tiempo mínimo tras restar la línea base describe una ejecución medida favorable. No establece el rendimiento típico bajo contención en producción, conjuntos de datos variados o un entorno informático diferente.
Tampoco se deriva una afirmación universal de rendimiento del resultado compartido de las pruebas. Una carga de trabajo con más entradas inusualmente largas podría activar la ruta alternativa con mayor frecuencia. Eso cambiaría el equilibrio del trabajo respecto a los valores con formato habitual evaluados.
Para SQLite parse_number(), por tanto, una lectura escéptica resulta constructiva. Los resultados respaldan examinar una implementación específica y reproducir su comportamiento. No eliminan la necesidad de definir formatos aceptados, conservar los registros rechazados y verificar los resultados en la aplicación anfitriona prevista.
Observe la reproducción, las revisiones de Rust y el comportamiento de la ruta alternativa
La próxima evidencia útil ampliaría la comparación manteniendo explícitos tanto el comportamiento como los métodos de medición.
La primera señal que conviene observar es la reproducción independiente. Una ejecución en otro sistema pondría a prueba hasta qué punto el orden reportado se mantiene ante cambios de hardware y entorno de compilación. Informar tanto los tiempos completos de consulta como los costes de análisis derivados haría esa evidencia más fácil de interpretar.
Resultados coincidentes reforzarían la confianza en que la ventaja del C optimizado se extiende más allá de la configuración original. Un orden diferente acotaría la conclusión e identificaría otro factor digno de investigación. Cualquiera de los dos resultados mejoraría la comparación si se documentaran el entorno y la distribución de entradas.
La reproducción también debería preservar la distinción entre resultados numéricos y tiempos. Un candidato rápido que modifica las entradas aceptadas ya no responde a la misma pregunta. Publicar los resultados de corrección junto con el rendimiento mantendría visible ese límite.
La segunda señal es una implementación revisada en Rust que aborde el trabajo específico visible en la comparación. Una mayor coincidencia en la estrategia de exploración y conversión ayudaría a separar la estructura de la implementación de la elección del lenguaje. La pregunta relevante es si una especialización comparable reduce la brecha medida.
Si una versión revisada de Rust reduce la diferencia mientras conserva sus ventajas de integración, el resultado inicial se convierte en un argumento más sólido para examinar los algoritmos antes de seleccionar lenguajes. Si el C optimizado conserva su liderazgo, los desarrolladores obtienen evidencia más firme para este componente concreto.
Esto sigue siendo una comparación propuesta, no un lanzamiento anunciado ni un resultado prometido. No hay motivo para asumir que una versión actualizada aparecerá en un calendario fijo. Su valor vendría de hacer que las implementaciones competidoras fueran más directamente comparables.
La tercera señal es la evidencia sobre el comportamiento de la ruta alternativa. El atajo optimizado en C maneja rápidamente las entradas elegibles, pero distribuciones de entrada más amplias pueden modificar la frecuencia con que se ejecuta la ruta más lenta. Probar esos casos revelaría más sobre el rango operativo práctico de la implementación.
Ese trabajo debería examinar la corrección tan de cerca como la velocidad. Las cadenas numéricas largas, los registros malformados y distintas configuraciones del entorno anfitrión pueden revelar supuestos que los valores comunes con formato no muestran. Un resultado documentado reforzaría la confianza más que otro tiempo destacado aislado.
Para los equipos de ingeniería, conservar la evidencia circundante importa tanto como conservar el código fuente. La configuración de compilación, los ejemplos de entrada y las alternativas descartadas explican por qué se seleccionó una implementación concreta. Guardarlos en una base de conocimientos consultable ofrece a los futuros revisores una forma de volver a evaluar esa decisión.
El valor inmediato de la investigación ya está claro: un cuello de botella medido condujo a una revisión específica y a un resultado modificado. Lo que sigue abierto es hasta dónde se extiende ese resultado. Las comparaciones posteriores deberían ampliar la evidencia sin convertir un experimento específico en un veredicto universal sobre lenguajes.
Antes de adoptar SQLite parse_number(), identifique la decisión a la que realmente se enfrenta su aplicación. Conserve entradas representativas, defina cómo deberían comportarse los porcentajes y los registros no válidos, y compare los candidatos dentro del entorno anfitrión que los ejecutará. Mantenga los tiempos completos de consulta junto a cualquier cifra de análisis derivada, para que un resultado favorable de un componente siga conectado con la experiencia del usuario. Revise la ruta numérica especializada con el mismo cuidado que su alternativa y conserve los detalles de compilación necesarios para reproducir ambas. El siguiente paso útil es comprobar si las ventajas de esta implementación resisten sus requisitos. ¿Qué restricción decide la elección en su aplicación: el tiempo de conversión repetida, el tamaño de distribución o el esfuerzo necesario para mantener el analizador?



