Skip to content

Exp 235: resume SWAR escape scan after each dirty chunk in json_write_string - #254

Open
danReynolds wants to merge 1 commit into
mainfrom
exp-235-json-escape-swar-resume
Open

Exp 235: resume SWAR escape scan after each dirty chunk in json_write_string#254
danReynolds wants to merge 1 commit into
mainfrom
exp-235-json-escape-swar-resume

Conversation

@danReynolds

Copy link
Copy Markdown
Owner

Hypothesis

While reading the selectBytes TEXT path I noticed resqlite_json_write_string
scans each TEXT cell for JSON-escapable bytes with a SWAR fast-skip that inspects
8 bytes at a time and skips whole clean chunks — the common case, text with no
escapes. But the loop was written as while (i + 8 <= len) { ... break; }: it
breaks to a trailing byte-by-byte loop the moment it sees the first
escapable byte, and never returns to the fast skip.

So a single escape early in an otherwise-safe value forces the entire remaining
tail through the one-byte-at-a-time path. A 256-byte log line with a newline near
the front, a multi-line chat message, a paragraph with a stray quote, JSON stored
as TEXT — all are >95% safe bytes but get crawled one byte at a time because the
scanner saw one escape early. I expected that re-entering the SWAR skip after the
escape would recover the fast path on the safe spans, winning on this (common)
sparse-escape shape while leaving the escape-free path untouched.

Approach

The scanner becomes an outer loop wrapping the original tight SWAR skip. The
inner while (i + 8 <= len) skip is kept verbatim (one bounds check per clean
chunk); on a miss it processes exactly the one offending 8-byte chunk (or the
final <8-byte tail) byte-by-byte, then the outer loop resumes the SWAR skip. A
sparse escape therefore only downgrades the single chunk that contains it; the
clean chunks on either side keep skipping 8 at a time.

Deliberately conservative: escape emission, the lookup tables, and \uXXXX
handling are unchanged, so output is byte-identical. Two design choices are
load-bearing for the guards — keeping the tight inner SWAR loop (an earlier
single-while structure with a doubled bounds check regressed the safe/late
guards +7%), and re-probing SWAR once per dirty chunk rather than per byte (a
first version re-probed per safe byte and regressed dense-escape +24%). Full
detail in experiments/235-json-escape-swar-resume.md.

Results

Two rounds of order-flipped focused passes (select_bytes_text_string_reserve.dart),
Apple Silicon. The load-bearing acceptance row (declared before measuring) is the
256B sparse-newline lane — realistic multi-line text with a \n every ~80 bytes.

Lane Δ (order-flipped)
pure-safe long ASCII 256B / 1KiB −3.4% / −1.7% common case — parity
sparse-newline 256B −23.4% load-bearing
sparse-newline 1KiB −31.4% confirmation
sparse-early 256B / 1KiB −44.7% / −44.1% confirmation
dense-escape 24B / 96B +2.1% / +0.1% guard — noise
dense-escape 256B +6.0% guard — pathological only

Realistic multi-line TEXT encodes ~1.3×–1.8× faster, and the one-early-escape
extreme is nearly 2× faster, because the old scanner turned a single early escape
into a full-length byte crawl and the new one does not. The escape-free common
path (the dominant real workload) is byte-identical and measures as parity. The
only real cost is dense-escape 256B at +6.0% — text where ~⅓ of bytes need
escaping pays one extra SWAR probe per all-dirty chunk; that distribution is not
natural text and is not in the release suite, so the realistic sparse win
dominates.

Outcome

Accepted (in review). A contained ~−23% (load-bearing) to −44% win on
sparse-escape TEXT selectBytes encoding — the common shape of multi-line and
lightly-quoted text — with the escape-free path provably unchanged and output
byte-identical. Cost is ~+6% on pathological dense-escape 256B only. No API,
platform, or allocation change; pure portable C. Would reopen the dense
regression only if a workload/profile shows dense-escape TEXT is a hot path.

Test plan

  • dart test test/native_encoder_diff_test.dart — 7/7 pass, including the
    byte-exact dart:convert oracle that injects escapes at positions
    {0, 15, 16, len−1} across lengths 0–1024 (directly covers the resume path)
  • dart test test/select_bytes_transfer_test.dart and
    dart test test/database_test.dart -N selectBytes — pass
  • dart analyze on changed sources — clean
  • dart run benchmark/finalize_experiment.dart --experiment=experiments/235-json-escape-swar-resume.md — green

@danReynolds danReynolds added type: performance Implementation experiment changing a runtime hot path approved Experiment succeeded: a kept win or a passing guard labels Jul 21, 2026
…_string

The selectBytes TEXT escaper's SWAR 8-byte fast-skip broke to the
byte-by-byte path on the first escapable byte and never resumed, so a
single early escape in a long value dragged the whole tail onto the slow
path. Re-enter the SWAR skip after each dirty chunk: a sparse escape now
only downgrades the one 8-byte chunk that contains it.

Two order-flipped focused rounds: -23.4% on the load-bearing 256B
sparse-newline lane (realistic multi-line text), -31% to -44% on the
sparse 1KiB/256B lanes, with the escape-free common path byte-identical
(parity). Cost is +6.0% on pathological dense-escape 256B text only (one
extra SWAR probe per all-dirty chunk). Pure portable C; no API/platform
change; output byte-identical (dart:convert oracle unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@danReynolds
danReynolds force-pushed the exp-235-json-escape-swar-resume branch from 8f95888 to 0a352ed Compare July 21, 2026 14:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Experiment succeeded: a kept win or a passing guard type: performance Implementation experiment changing a runtime hot path

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant