top of page

SQLite parse_number(): C Beats Rust After a Parser Rewrite

23 minutes ago
11 min read

Simon Willison’s SQLite parse_number() experiment initially put Rust ahead of C, until a targeted parser rewrite reversed the performance ranking. Published November 8, 2026, the investigation compares extensions, compiled add-ons that introduce database functions, for cleaning formatted numbers. The resulting C implementation reportedly led every tested dataset while retaining a smaller compiled file.

The task sounds modest: turn text containing currency symbols, commas, or percent signs into values suitable for numerical sorting and addition. Yet the comparison exposed different answers to different engineering questions. Rust delivered the faster first decimal parser; optimized C ultimately delivered the fastest measured implementation.

For developers evaluating AI-generated code, the pressure falls on the review process. A plausible implementation and a familiar language provide little evidence about where execution time goes. Willison’s investigation became useful when measurement identified a specific conversion routine and prompted a narrower experiment.

SQLite parse_number() changes the result after a second pass

The reported winner changed when the implementation changed, making the parser design central to the comparison.

Willison began with a request to Claude, identified in his account as Opus 5.5, to explore the function in C and Rust. His requested comparison covered execution speed, code complexity, and the resulting library’s size. He describes the investigation in his SQLite parsing experiment.

The first results separated file size from speed. Willison reported an approximately 14 KB C extension, compared with 321 KB using rusqlite and 298 KB using sqlite-loadable-rs. Those Rust libraries connect Rust code to SQLite. Despite its smaller file, the initial C implementation lost on mixed and decimal inputs.

The explanation centered on strtod(), a C library function that converts text into a floating-point number, an approximate numerical representation. Willison attributed Rust’s initial decimal advantage to its faster conversion routine. He then asked Claude to produce a C parser that matched or exceeded the Rust implementation.

The revised C version led the reported comparison. Over mixed data, rounded net costs fell from 67 nanoseconds per row for initial C to 19 for revised C. The Rust versions recorded 46 and 41. Decimal-only inputs showed an even larger change within C, from 106 to 18 nanoseconds per row.

That sequence gives the story its significance. Stopping after the first comparison would support choosing Rust for the measured decimal workload. Continuing with a targeted revision produced a different candidate, without changing the requested database function.

The exercise also separates generating alternatives from evaluating them. Claude supplied implementations, but their relative value emerged through the surrounding measurements. The initial answer supplied a starting point rather than a settled engineering decision.

There is no basis here for treating the outcome as a universal ranking of programming languages. These are particular implementations of a small function. Their differences include conversion routines, temporary storage, and integration code, all of which sit beneath a broad C-versus-Rust headline.

The practical change is more specific: developers have a published example where a slow first C draft became the fastest measured candidate. That makes the case useful for examining implementation choices, while leaving broader language claims unsupported.

The benchmark measures repeated parsing inside SQLite

The workload gives repeated conversion a central role, so its results need that context attached.

The published benchmark harness creates an in-memory database containing 2 million rows. It evaluates a query that parses each value and sums the results. Python starts the query, while SQLite performs the repeated database work.

This arrangement matters because it avoids placing a Python loop around every individual conversion. The experiment therefore targets the combined database function call and parsing path more directly. It does not primarily measure a Python application repeatedly crossing into native code.

The harness alternates candidates across 15 runs. It records minimum and median elapsed times, then calculates the advertised net figure from the minimum. A baseline scan that sums string lengths is subtracted before the result becomes nanoseconds per row.

That subtraction helps isolate incremental work, but the figure remains a comparative estimate. It is not the complete duration of an import, a dashboard refresh, or a query involving other operations. Reading it as a user-facing response time would extend the measurement beyond its design.

The datasets also shape the result. Separate runs cover integer text, decimal text, and mixed formatted values. The mixed generator includes currency-marked values, percentages, ordinary numerical strings, and invalid entries. Those variations make it possible to distinguish a decimal conversion problem from a general parsing problem.

Consider an application that repeatedly sorts imported financial reports. If parsing occurs during every query, conversion work becomes part of each report request. If the application instead normalizes values during import, the same parser affects a different stage and faces a different performance requirement.

Neither architecture automatically follows from these measurements. The experiment supplies evidence about the parsing component, while the application determines how frequently that component runs. A faster isolated function matters most when it occupies a meaningful share of the actual workload.

The benchmark’s pure SQL alternative uses nested replace() calls, meaning repeated string substitutions, followed by numerical conversion. Its presence gives developers a familiar comparison point. However, replacing characters and enforcing a complete input policy are different jobs, so equivalent behavior needs separate examination.

The pressure therefore falls on developers who use short benchmark summaries to justify implementation choices. Before adopting the reported ranking, they need to identify where parsing happens in their application. They also need to determine whether their inputs resemble the generated datasets.

For SQLite parse_number(), the harness makes those questions inspectable. Readers can examine both the timed query and the data generator, rather than relying solely on a winning number.

C gains speed by combining validation and conversion

The optimized C implementation reduces intermediate work while retaining a fallback for inputs outside its shortcut’s limits.

The optimized decimal parser inspects characters while accumulating numerical information. During that scan, it tracks significant digits and counts digits following the decimal point. This lets suitable inputs avoid reconstruction as a cleaned string.

Its shortcut uses Clinger’s fast path, a decimal-conversion technique that applies when intermediate values meet exact-representation requirements. The implementation restricts this path using explicit numerical conditions. Values outside those conditions continue through a slower conversion route.

That distinction is essential. A shortcut that simply accepts rounding errors would answer a different engineering question. Here, the code attempts to accelerate a restricted set of inputs while preserving the intended numerical result.

The implementation collects up to 19 significant digits. Its decimal shortcut additionally requires an accumulated value no greater than 2^53 and no more than 22 fractional digits. These boundaries determine when the direct calculation is eligible.

The broader design explains why the revision changed the comparison. Validation already requires examining the input’s characters. Accumulating useful numerical information during that examination avoids some work that a separate conversion stage would otherwise repeat.

Integer handling benefits from the same approach. When the value fits, the function can return the accumulated integer directly. It does not need to build a temporary cleaned string before converting that string again.

The fallback remains substantial engineering work. It reconstructs validated numerical text, uses a local buffer when possible, and allocates additional storage when necessary. Allocation here means requesting temporary memory for the operation, which introduces handling beyond the arithmetic itself.

The code also checks the compiler’s floating-point evaluation behavior before enabling the shortcut. Its guard addresses the possibility that intermediate calculations use a different precision. That detail shows how a small arithmetic optimization can acquire obligations beyond its apparent line count.

Rust remains a meaningful comparison because its candidates use a different arrangement of work. The investigation does not establish that an equally specialized Rust rewrite would retain the same timing. Comparing optimized C against these Rust versions establishes a result for those candidates.

A reasonable interpretation is that specialization paid off for the tested workload. The C rewrite combines tasks that were previously separated and avoids a general conversion routine for eligible values. That interpretation follows the mechanism visible in the implementation.

It also explains why language reputation provides an incomplete prediction. General routines offer behavior across a broad input space; specialized code can target a narrower contract. Performance depends partly on how much work the chosen route performs for the inputs that actually arrive.

The maintenance question follows directly. Whoever owns the faster implementation also owns its eligibility conditions, fallback behavior, and numerical assumptions. The measured advantage becomes useful only if the team can preserve those properties through later changes.

Extension size and code ownership pull in different directions

The smaller compiled artifact and the simpler maintenance path are separate criteria, and neither substitutes for the other.

The project’s implementation comparison reports stripped C libraries of 14,408 bytes. The corresponding Rust artifacts measure 320,864 bytes with rusqlite and 298,464 bytes with sqlite-loadable. Stripping removes certain build information from a compiled file.

The same report counts 75 nonblank, noncomment lines for straightforward C and 104 for optimized C. Rust uses 36 or 30 integration lines plus a shared 41-line parser. It reports 20 dependencies for the rusqlite version and 62 for sqlite-loadable.

Those measurements describe different forms of complexity. Source length captures visible implementation volume; dependency count captures part of the build graph. Neither directly measures how difficult a future bug will be to diagnose.

A short function can depend on behavior distributed across libraries. A longer self-contained function can concentrate responsibility in code the application team must review. Choosing between them requires asking where the team wants that responsibility to reside.

The C implementation makes several responsibilities visible. Its buffer handling, integer range checks, and decimal shortcut appear in the project’s own source. Reviewers can inspect them directly, but visibility does not remove the need for expertise.

Rust’s language facilities and parsing library can reduce some manual handling. That can improve readability for a team already working in Rust. However, a compact integration layer still needs review for input policy, database behavior, and error handling.

Compiled size creates another question. An additional library has different significance in a tightly constrained distribution than in an application already carrying substantial assets. The reported difference is measurable; its practical importance depends on the delivery environment.

File size also cannot stand in for runtime memory consumption. A library’s bytes on disk do not describe every allocation made while processing data. Treating the smallest file as the lowest-memory implementation would require measurements this comparison does not supply.

The same caution applies to dependency counts. More dependencies can introduce more build and update considerations, but counting them does not establish defect rates. Fewer dependencies likewise do not establish that application-owned code is easier to maintain.

For an existing C application, a compact C extension might fit established compilation and review practices. For an existing Rust application, introducing a separate C component might create additional ownership work. These are deployment judgments, rather than outcomes established by the benchmark.

SQLite parse_number() therefore presents a decision with several independent dimensions. The reported optimized C candidate leads on measured parsing and compiled size. A team still needs to decide whether maintaining its specialized conversion path fits its skills and release process.

The useful comparison keeps those dimensions visible. Collapsing them into a single winner would conceal the reasons different teams could reasonably make different choices.

Passing tests does not settle the data contract

Correctness includes the meaning assigned to formatted text, beyond whether implementations agree on a numerical answer.

The comparison report says all versions passed 40 explicit cases and 8 million randomized comparisons. Randomized testing, often called fuzzing, generates inputs to search for unexpected behavior. Those reported results provide evidence, although they have not been independently reproduced for this article.

Agreement across implementations is valuable, especially after an arithmetic rewrite. It makes obvious divergence less likely within the tested input space. Yet shared agreement cannot determine whether the selected behavior matches a particular application’s business rules.

Percentages illustrate the distinction. Removing a percent sign preserves the displayed number, while interpreting a percentage as a ratio changes its scale. An application must decide which meaning it needs before treating either behavior as correct.

Currency markings raise a related issue. Removing a symbol makes text easier to process numerically, but it does not establish that values share the same unit. Combining amounts from different currencies requires information that character removal alone cannot supply.

Formatting conventions also deserve attention. A comma can mark digit grouping in one convention and a decimal boundary in another. A parser designed around a fixed convention needs that expectation attached to the data it receives.

These are hypothetical adoption concerns, not defects established in Willison’s implementation. They explain why a parser can perform exactly as designed and still be inappropriate for an unfamiliar dataset. Correctness begins with the selected contract.

Numerical precision adds another boundary. SQLite’s floating-point documentation explains that its REAL values use binary-64 representation and are approximate. Many decimal fractions lack an exact representation in that format, regardless of the language performing the conversion.

A correctly rounded parser selects the appropriate representable value. It does not make all subsequent arithmetic exact. Applications requiring exact decimal calculations need to evaluate their representation and arithmetic requirements separately.

Failure handling deserves the same attention. Returning NULL, SQLite’s marker for an absent or unknown value, can let processing continue when text is invalid. An application should still preserve a way to distinguish rejected data from valid numerical input.

Otherwise, a clean-looking result can hide a data-quality problem. A user examining a total needs to know whether every expected record contributed meaningfully. That concern belongs to the surrounding application, even when the parser follows its contract.

The benchmark itself introduces a separate uncertainty. Minimum timing after baseline subtraction describes a favorable measured execution. It does not establish typical performance under production contention, varied datasets, or a different computing environment.

No universal performance claim follows from the shared test result either. A workload containing more unusually long inputs might exercise the fallback more often. That would change the balance of work relative to the tested, commonly formatted values.

For SQLite parse_number(), the skeptical reading is therefore constructive. The results support examining a specific implementation and reproducing its behavior. They do not remove the need to define accepted formats, preserve rejected records, and verify results in the intended host application.

Watch reproduction, Rust revisions, and fallback behavior

The next useful evidence would broaden the comparison while keeping behavior and measurement methods explicit.

The first signal to watch is independent reproduction. A run on another system would test how well the reported ordering survives changes in hardware and build environment. Reporting both complete query times and derived parsing costs would make that evidence easier to interpret.

Matching results would strengthen confidence that the optimized C advantage extends beyond the original setup. A different ordering would narrow the conclusion and identify another factor worth investigating. Either outcome would improve the comparison if the environment and input distribution were documented.

Reproduction should also preserve the distinction between numerical results and timing. A fast candidate that changes accepted inputs is no longer answering the same question. Publishing correctness outcomes alongside performance would keep that boundary visible.

The second signal is a revised Rust implementation addressing the specific work visible in the comparison. A closer match in scanning and conversion strategy would help separate implementation structure from language choice. The relevant question is whether comparable specialization narrows the measured gap.

If revised Rust closes the difference while retaining its integration benefits, the initial result becomes a stronger argument for examining algorithms before selecting languages. If optimized C retains its lead, developers gain firmer evidence for this particular component.

That remains a proposed comparison, not an announced release or promised result. There is no reason to assume an updated version will appear on a fixed schedule. Its value would come from making the competing implementations more directly comparable.

The third signal is evidence about fallback behavior. The optimized C shortcut handles eligible inputs quickly, but broader input distributions can alter how often the slower route runs. Testing those cases would reveal more about the implementation’s practical operating range.

That work should examine correctness as closely as speed. Long numerical strings, malformed records, and different host settings can expose assumptions that common formatted values do not. A documented result would strengthen confidence more than another isolated headline timing.

For engineering teams, retaining the surrounding evidence matters as much as retaining the source. Build settings, input examples, and rejected alternatives explain why a particular implementation was selected. Keeping them in a searchable knowledge base gives future reviewers a way to revisit that decision.

The investigation’s immediate value is already clear: a measured bottleneck led to a specific revision and a changed outcome. What remains open is how broadly that outcome travels. Further comparisons should expand the evidence without turning a focused experiment into a universal language verdict.

Before adopting SQLite parse_number(), identify the decision your application actually faces. Preserve representative inputs, define how percentages and invalid records should behave, and compare candidates inside the host that will run them. Keep complete query timings alongside any derived parsing figures, so a favorable component result stays connected to user experience. Review the specialized numerical path with the same care as its fallback, and retain the build details needed to reproduce both. The useful next step is to test whether this implementation’s advantages survive your requirements. Which constraint decides the choice in your application: repeated conversion time, distribution size, or the effort required to maintain the parser?

Give every agent the context to do better work

Connect your agents to the knowledge, decisions, and history already organized in remio.

remio currently supports Windows 10+ (x64) and Macs with Apple silicon.

Your AI Partner at Work
Get more done with remio

Plan. Create. Deliver.
All in one place.

bottom of page