Skip to content

perf: fast integer path in number-to-string for ~38% stringops speedup#505

Merged
cs01 merged 1 commit intomainfrom
perf/stringops
Apr 13, 2026
Merged

perf: fast integer path in number-to-string for ~38% stringops speedup#505
cs01 merged 1 commit intomainfrom
perf/stringops

Conversation

@cs01
Copy link
Copy Markdown
Owner

@cs01 cs01 commented Apr 13, 2026

User-visible win

Programs that build strings by concatenating numbers (e.g. `"item" + i`, `"row-" + id`, logging loops, CSV/JSON formatting) now run noticeably faster. On the `stringops` benchmark:

  • Before: ~17-18 ms
  • After: ~10-11 ms
  • ~38% faster — now within ~1.7x of hand-written C (~6 ms), down from ~3x.

Any app that formats numbers into strings in a hot loop benefits.

Root cause

`sample` profiling of the stringops benchmark showed ~45% of runtime inside `snprintf` — specifically `__vfprintf` / `__Balloc_D2A` / `__d2b_D2A` / `localeconv_l`. Every `"item" + i` concatenation was calling `snprintf("%.15g", double)`, which on macOS/glibc goes through a slow locale-aware floating-point path even when the value is a small integer.

The old `convertNumberToString` codegen also emitted:

  1. `alloca` + `snprintf`
  2. `strlen` on the result
  3. `cs_arena_alloc`
  4. `strcpy` (another full scan)

Four passes over the buffer per number.

Fix

  • Added `cs_num_to_str(double)` bridge in `c_bridges/string-ops-bridge.c`. It takes the common fast path for integer-valued doubles (JS-compatible: no trailing `.0`, `-0` → `"0"`, safe range check against ±2^53), doing a direct itoa into an arena buffer with a single memcpy — no snprintf, no locale, no strlen, no strcpy.
  • Non-integers, NaN, and Infinity still fall through to `snprintf("%.15g", ...)` inside the bridge, preserving exact semantics.
  • `convertNumberToString` in the codegen is now a single `call @cs_num_to_str` instead of 7 emitted instructions.

Validation

  • `npm test` — all 774 tests pass
  • `npm run verify:quick` — PASSED (TypeScript + Stage 0 + Stage 1 self-hosting + tests)
  • Sanity-checked `fibonacci`, `sieve`, `fileio`, `json`, `matmul` benchmarks — no regressions
  • `stringops` phase breakdown (Date.now probes), integer-concat loop:
    • Before: ~10.2 ms
    • After: ~2.4 ms (~4x on the phase)

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark Results (Linux x86-64)

Benchmark C ChadScript Go Node Place
Binary Trees 1.390s 1.248s 2.654s 1.206s 🥈
Cold Start 0.8ms 0.9ms 1.2ms 28.0ms 🥈
Fibonacci 0.911s 0.908s 1.731s 3.365s 🥇
File I/O 0.091s 0.097s 0.092s 0.176s 🥉
JSON Parse/Stringify 0.004s 0.005s 0.016s 0.015s 🥈
Matrix Multiply 0.493s 0.518s 0.787s 0.376s 🥉
Monte Carlo Pi 0.439s 0.441s 0.458s 2.601s 🥈
N-Body Simulation 1.760s 2.271s 2.282s 2.392s 🥈
Quicksort 0.245s 0.283s 0.242s 0.300s 🥉
SQLite 0.327s 0.363s 0.412s 🥈
Sieve of Eratosthenes 0.014s 0.027s 0.020s 0.042s 🥉
String Manipulation 0.008s 0.026s 0.016s 0.038s 🥉

CLI Tool Benchmarks

Benchmark ChadScript grep node xxd Place
Hex Dump 0.433s 0.924s 0.140s 🥈
Recursive Grep 0.020s 0.011s 0.101s 🥈

@cs01 cs01 merged commit 3447431 into main Apr 13, 2026
13 checks passed
cs01 added a commit that referenced this pull request Apr 13, 2026
Co-authored-by: cs01 <cs01@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant