Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize String write #1651

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open

Optimize String write #1651

wants to merge 6 commits into from

Conversation

vbabanin
Copy link
Member

@vbabanin vbabanin commented Mar 18, 2025

Summary

This PR optimizes string writing in BSON by minimizing redundant checks and internal method indirection in hot paths.

Key Changes:

  • Removed internal method indirection (e.g., write() wrappers) that introduced extra bound checks.
  • Avoided repeated calls to ensureOpen(), hasRemaining(), and related checks by caching position and limit values inside hot paths.
  • When writing ASCII, write directly to the underlying ByteBuffer array if capacity allows. This avoids the extra bounds and range checks in put(). Logic follows the same fast path approach used in String.getBytes() for ASCII.
  • Used str.charAt() instead of Character.toCodePoint() to avoid unnecessary surrogate checks when not needed (e.g., for characters within the ASCII or 2-byte UTF-8 code unit range). Fall back only when multi-unit characters (e.g., 3-byte UTF-8) are detected.

Performance analysis

To ensure accurate performance comparison and reduce noise, 11 versions (Comparison Versions) were aggregated and compared against a stable region of data around the Base Mainline Version. The percentage difference and z-score of the mean of the Comparison Versions were calculated relative to the Base Mainline Version’s stable region mean.

The following tables show improvements across two configurations:

  • The first uses ASCII-only strings from the regular benchmark suite.
  • The second uses an augmented benchmark dataset with UTF-8 strings containing 3-byte characters.

ASCII Benchmark Suite (Regular JSON Workloads)

Test Base Mean (MB/s) Patched Mean (ops/s) Diff Z-Score
Deep BSON Encoding 47.02 56.97 +21.2% 2.97
Flat BSON Encoding 105.49 238.03 +125.6% 6.65
Full BSON Encoding 110.68 176.65 +59.6% 4.21
Large Doc Bulk Insert 46.75 62.48 +33.6% 2.99
Large Doc InsertOne 50.52 67.18 +33.0% 3.03
Small Doc Bulk Insert 25.09 28.30 +12.8% 2.07

Perf analyzer results: Link

Augmented Benchmark Suite (UTF-8 Strings with 3-Byte Characters)

To evaluate performance on multi-byte UTF-8 input, the large_doc.json, small_doc.json, and tweet.json datasets were modified to use UTF-8 characters encoded with 3 bytes (code units). These changes were introduced on mainline via an Evergreen patch, and benchmark results were collected from:

  • 11 runs on patched mainline (baseline with UTF-8 input)
  • 11 runs on this branch (UTF-8 input + string write optimizations)
Test Base Mean (MB/s) Patched Mean (ops/s) Diff Z-Score
Deep BSON Encoding 91.31 124.3 +35.8% 6.94
Flat BSON Encoding 95.79 193.47 +100.2% 13.2
Full BSON Encoding 86.3 151.8 +73.4% 9.76
Large Doc Bulk Insert 35.5 46.0 +31.8% 8.79
Large Doc InsertOne 42.8 53.28 +25.9% 8.06
Small Doc Bulk Insert 43.6 53.85 +23.6% 8.45

Perf analyzer results: Link

JAVA-5816

- Remove extra bounds checking.
- Add ASCII fast-loop similar to String.getBytes().
@franz1981
Copy link

That's related to #1629 🙏

@vbabanin vbabanin self-assigned this Mar 20, 2025
…ncoding tests.

- Adjust logic to handle non-zero ByteBuffer.arrayOffset, as some Netty Pooled ByteBuffer implementations return an offset != 0.
- Add unit tests for UTF-8 encoding across buffer boundaries and for malformed surrogate pairs.
- Fix issue with a leacked reference count on ByteBufs in the pipe() method (2 non-released reference counts were retained).

JAVA-5816
@vbabanin vbabanin changed the title writeCharacters for NIO Buffers. Optimize String write Mar 23, 2025
@vbabanin vbabanin requested a review from rozza March 25, 2025 14:44

if (c < 0x80) {
if (remaining == 0) {
buf = getNextByteBuffer();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still suggest to give at shot at the PR I made which use a separate index to access single bytes in the internalNio Buffer within the Netty buffers, for two reasons:

  • NIO ByteBuffer can benefit from additional optimizations from the JVM since it's a known type to it
  • Netty buffer read/write both move forward the internal indexes and force Netty to verify accessibility of the buffer for each operation, which have some Java Memory Model effects (.e.g. any subsequent load has to happen for real, each time!)

@vbabanin vbabanin marked this pull request as ready for review March 26, 2025 05:22
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.

2 participants