Software Engineering

Simon Willison benchmarks C vs Rust SQLite extensions for parse_number()

Key Takeaway

A hand-optimized C SQLite extension for parsing formatted numbers outperformed Rust implementations by 2x after replacing strtod() with a custom parser, though Rust's standard library decimal parsing was initially faster than C's.

Summary

Developer Simon Willison published a research comparison of four SQLite extensions implementing a parse_number() function that strips currency symbols, commas, and percent signs from strings like "-$1,234.56" or "12.5%" to produce sortable numeric values. The project compared two Rust approaches using rusqlite (321 KB) and sqlite-loadable-rs (298 KB) against an initial C implementation (14 KB) and an optimized C rewrite. The initial C version lagged behind Rust because C's strtod() was roughly half the speed of Rust's equivalent decimal parser, particularly on decimal-heavy data where it reached 106 ns per row versus 47-55 ns for Rust. After prompting Claude Opus 5.5 to write a custom C text-to-decimal parser, the optimized C extension achieved 16-19 ns per row across mixed, integer-only, and decimal-only datasets—approximately twice as fast as both Rust variants and dramatically faster than pure SQL replace() chains.

All four implementations were validated against identical test suites covering type preservation, parsing rules, and floating-point accuracy, plus 8 million fuzz checks. The resulting research report and code were published to Willison's simonw/research repository. The experiment also served as a practical demonstration of using large language models to iteratively refine performance-critical systems code.

Performance characteristics and implementation trade-offs

  • Binary size: The C extension compiled to 14 KB, while Rust builds using rusqlite and sqlite-loadable-rs produced 321 KB and 298 KB respectively, reflecting Rust's standard library and crate dependencies.
  • Initial C bottleneck: The first C implementation relied on strtod() for decimal parsing, which proved substantially slower than Rust's equivalent—particularly on decimal-only data where it reached 106 ns per row versus Rust's 47-55 ns.
  • Optimized C parser: A custom C text-to-decimal parser eliminated the strtod() bottleneck, reducing per-row cost to 16-19 ns across all data types, roughly 2x faster than both Rust implementations.
  • Validation methodology: All versions passed identical tests for type preservation, parsing rule correctness, and floating-point accuracy, plus 8 million fuzz-generated inputs to ensure behavioral parity.
  • Build ecosystem: The Rust versions leveraged rusqlite and sqlite-loadable-rs crates for SQLite FFI bindings, while the C version used SQLite's native C extension API directly with no external dependencies.

Why it matters

This comparison provides concrete data for developers choosing between C and Rust for SQLite extension development, a common decision point in database tooling and embedded analytics. The 20x binary size difference versus 2x performance inversion after optimization challenges assumptions about Rust's default superiority in systems programming, while demonstrating that targeted C optimization can reclaim performance leadership. For Linux distribution packagers and database engineers, the published research report offers reproducible methodology and code for evaluating extension performance in data-wrangling pipelines.

Original reference: simonwillison.net →