Skip to content

Speed up Slice search, UTF-8 case conversion, and trims - #196

Open
wendigo wants to merge 10 commits into
airlift:masterfrom
wendigo:user/serafin/fixes
Open

Speed up Slice search, UTF-8 case conversion, and trims#196
wendigo wants to merge 10 commits into
airlift:masterfrom
wendigo:user/serafin/fixes

Conversation

@wendigo

@wendigo wendigo commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

A batch of performance improvements to Slice and SliceUtf8, plus one bug fix. Each commit is independent and individually benchmarked (JMH, JDK 25, Apple Silicon; multi-fork runs where single-fork variance was significant).

Bug fix

  • Slices.ensureSize threw ArrayIndexOutOfBoundsException when growing a view slice whose backing array extends more than the new capacity past the view start — the tail zero-fill used a bound in the old array's coordinates. Now clamped; regression test added.

Search

  • indexOf: replaced the 4-byte scan window with an 8-byte SWAR loop using exact zero-byte detection (no false-positive candidates), candidate iteration via bit tricks instead of re-reading memory, and a new tail anchor (last four pattern bytes) alongside the head anchor to reject false candidates before the full comparison. 1.2–4× faster across sparse and repetitive inputs; scan throughput ~4 → ~11 GB/s.
  • lastIndexOfByte: switched to the exact zero-byte mask, eliminating the per-candidate re-check loop. 2.9× when the match sits below bytes differing only in the lowest bit (e.g. n vs o), 1.19× on plain backward scans.
  • lastIndexOf: same head/tail anchors as indexOf. ~1.7× on repetitive inputs whose candidates fail deep in the pattern; unchanged on random data.

UTF-8

  • ASCII case conversion (toUpperCase/toLowerCase): scans and translates eight bytes at a time by flipping the case bit of bytes in range (SWAR range test, branchless tail). 2.7–3.7× at 100 B, 5–9× at 1 KB+; the zero-copy return for already-correct input is preserved and also 8× faster to reach. Non-ASCII inputs unchanged.
  • Case-mapping and whitespace tables: the four flat int[0x110000]/boolean[0x110000] arrays (≈13.7 MB of statics) become 256-entry pages where unmapped pages share a single zero/empty page — ≈160 KB total (33–36 distinct pages per case table). Trade-off: translating uniformly random non-ASCII code points measures up to 10% slower from the extra indirection; ASCII paths and trims are unchanged.
  • Trims: whitespace scanning uses a SWAR stop mask over the two contiguous ASCII whitespace ranges, and the forward scan skips homogeneous runs (e.g. space padding) with vectorized Arrays.mismatch. 1.8–3.8× on ASCII whitespace at 100 B+. Trade-off: inputs of randomly interleaved multi-byte whitespace measure 4–19% slower (unpredictable run-detection branch); noted in the commit message.

I/O

  • DynamicSliceOutput.writeZero: one ensureSize + one Arrays.fill instead of a writeLong(0) loop that re-entered ensureSize every eight bytes — ~2×.
  • BasicSliceInput.readByte: single bounds check and unchecked read instead of routing through read()'s −1 sentinel and a second check; semantics (including the exception at end of input) unchanged.

@wendigo
wendigo requested a review from dain July 12, 2026 22:24
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7299bce8-85ab-4609-8a09-b89a003f128f

📥 Commits

Reviewing files that changed from the base of the PR and between 95a137d and 7b32fa1.

📒 Files selected for processing (9)
  • src/main/java/io/airlift/slice/BasicSliceInput.java
  • src/main/java/io/airlift/slice/DynamicSliceOutput.java
  • src/main/java/io/airlift/slice/Slice.java
  • src/main/java/io/airlift/slice/SliceUtf8.java
  • src/main/java/io/airlift/slice/Slices.java
  • src/test/java/io/airlift/slice/TestSlice.java
  • src/test/java/io/airlift/slice/TestSliceOutput.java
  • src/test/java/io/airlift/slice/TestSliceUtf8.java
  • src/test/java/io/airlift/slice/TestSlices.java

📝 Walkthrough

Walkthrough

The changes optimize forward and reverse slice searches with word-based candidate detection and anchor verification. UTF-8 case conversion and whitespace trimming now use paged lookup data and word-at-a-time scanning. Basic slice input uses explicit bounds checks with unchecked reads, while dynamic zero writing validates lengths and clears backing regions directly. Slice growth padding is bounded, and randomized and regression tests cover these behaviors.

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

wendigo added 10 commits July 14, 2026 23:21
toUpperCase and toLowerCase now scan and translate ASCII input eight bytes
at a time by flipping the case bit of bytes in the cased range. This makes
case conversion of ASCII inputs 3-9x faster at 100+ bytes while keeping
non-ASCII inputs on the existing code-point path.
indexOf now scans eight-byte windows with an exact SWAR zero-byte mask,
visiting candidate positions via bit iteration instead of re-reading
memory, and rejects false candidates with head and tail anchors before
the full comparison. This makes substring search 1.2-4x faster across
sparse and repetitive inputs.
The exact SWAR mask has no false positives, so the highest set bit is
the last occurrence and the per-candidate re-check loop is unnecessary.
This makes matches near ambiguous bytes ~3x faster and backward scans
~20% faster.
readByte checks the position once and reads unchecked instead of going
through read() and its -1 sentinel, which paid a second bounds check in
the checked getByte. Semantics are unchanged, including the exception
thrown at end of input.
writeZero now ensures capacity once and clears the range with a single
Arrays.fill instead of looping writeLong(0), which re-entered ensureSize
for every eight bytes. This makes writeZero about 2x faster.
The lower, upper, and title case tables are now 256-entry pages of deltas
where every unmapped page shares a single zero page, shrinking the tables
from 12.7 MB of flat arrays to about 150 KB (33-36 distinct pages each).
Translation of uniformly random non-ASCII code points measures up to 10%
slower due to the extra indirection, while ASCII paths are unchanged.
The whitespace table becomes 256-entry pages where pages without any
whitespace share a single empty page, shrinking it from 1 MB to a few
kilobytes. Trim and title-case behavior is unchanged and benchmarks are
within noise.
lastIndexOf now compares the first and last four pattern bytes before
running the full comparison, mirroring indexOf. Repetitive inputs whose
candidates fail deep into the pattern search about 1.7x faster, while
random inputs are unchanged.
Trim scans skip ASCII whitespace with a SWAR stop mask over the two
contiguous whitespace ranges, and the forward scan skips homogeneous
runs with the vectorized Arrays.mismatch. ASCII whitespace trims at 100+
bytes get 1.8-3.8x faster, while inputs of randomly mixed multi-byte
whitespace measure up to 19% slower due to the unpredictable run
detection.
@wendigo
wendigo force-pushed the user/serafin/fixes branch from 7b32fa1 to d1c0b9b Compare July 14, 2026 21:28
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