Skip to content

perf: remove remaining variable-size memmove/memset in to_chars - #2808

Open
lemire wants to merge 4 commits into
masterfrom
lemire/to_chars_no_memmove
Open

perf: remove remaining variable-size memmove/memset in to_chars#2808
lemire wants to merge 4 commits into
masterfrom
lemire/to_chars_no_memmove

Conversation

@lemire

@lemire lemire commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #2805. That PR inlined the hot-path memcpy/memset/memmove in dragonbox() / format_buffer() by using compile-time sizes and a 40-byte caller scratch buffer. Two cold paths still used variable-length memmove/memset:

  1. 0.[000]digits (small magnitudes with leading zeros before the digit string)
  2. scientific d.igitsE±xx (insert . after the leading digit)

This PR replaces those with fixed-size copies (via a small stack temp when the shift overlaps) so all of to_chars format paths can be inlined. On big4 (GCC 14, Release), objdump of simdjson::internal::to_chars goes from 2 libc mem* calls to 0.

Also documents and names the required caller capacity as simdjson::internal::to_chars_buffer_size (40): only ~24 characters are ever part of the logical result, but fixed-size 16/17-byte over-writes need the extra scratch for safety. Call sites in DOM serialization and string_builder use the constant.

Changes

  • src/to_chars.cpp: remove remaining variable-size memmove/memset; clarify over-write contract
  • include/simdjson/common_defs.h: add to_chars_buffer_size + API notes
  • Callers: serialization-inl.h, json_string_builder-inl.h use the named constant

Correctness (big4)

Benchmarks (big4)

Host: Intel Xeon Gold 6548N, g++ 14.3.1, Release, taskset -c 4, 5 interleaved rounds × 3 trials of benchmark_car_builder.

master (#2805) this PR Δ
ns/char (avg) 1.121 1.115 −0.6%
GB/s (avg) 0.891 0.899 +0.9%
cycles/char 3.873 3.844 −0.8%
ins/char 15.56 15.35 −1.4%
libc mem* in to_chars 2 0

car_builder is mostly normal magnitudes, so wall time is nearly flat; instruction count drops a bit and the last libc mem* calls disappear from to_chars.

Test plan

  • builder_string_builder_tests
  • random_string_number_tests
  • random finite double format identity vs master
  • benchmark_car_builder A/B on big4
  • CI

lemire added 4 commits August 4, 2026 14:15
Finish the work started in #2805: the cold format_buffer paths for
0.[000]digits and scientific notation still called memmove/memset with
runtime lengths. Use fixed-size copies (with a small temp for the
overlapping shifts) so those calls can be inlined as well.

Document and name the required 40-byte caller scratch as
internal::to_chars_buffer_size: only ~24 characters are emitted, but
fixed-size 16/17-byte over-writes need the extra room for safety.
inline constexpr is a C++17 feature. CI still builds C++11 targets
(readme_examples11, quickstart11, etc.) with -Werror=c++17-extensions,
which rejected the constant. Use static constexpr instead.
CI failed on simdjson-singleheader with -Wunused-const-variable:
static constexpr to_chars_buffer_size is only referenced from header
callers, so it is unused in the amalgamated .cpp TU under -Werror.

Use an unscoped enum (C++11-friendly, never "unused") and regenerate
the singleheader amalgamation so the committed files match the sources.
just_ascii fails when .cpp sources contain non-ASCII. Re-amalgamation
copied an em dash from portability.h into singleheader/simdjson.cpp.
Replace it with ASCII -- so CI passes.
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