Perf: Allocation-free MONEY and DECIMAL processing with scratch-buffer String reads in the ResultSet read path - #2991
Open
Ananya2 wants to merge 3 commits into
Open
Perf: Allocation-free MONEY and DECIMAL processing with scratch-buffer String reads in the ResultSet read path#2991Ananya2 wants to merge 3 commits into
Ananya2 wants to merge 3 commits into
Conversation
…er String reads in the ResultSet read path
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces allocation-reducing optimizations in the ResultSet read/convert path of the Microsoft JDBC Driver for SQL Server, targeting high-volume wide-row scans by avoiding per-cell temporary objects while preserving existing decoding/streaming behavior.
Changes:
- Add a scratch-buffer-backed
readStringFromByteshelper to avoid per-cellbyte[]allocations for small synchronous textualgetString()reads. - Replace
MONEY/SMALLMONEYdecoding paths that usedBigIntegerintermediates with directlong-backedBigDecimal.valueOf(..., 4)construction (including Always Encrypted denormalization). - Add a
BigDecimalwire-encoding fast path that emits magnitude bytes directly when the magnitude fits in along, skippingBigInteger.toByteArray()allocation. - Skip redundant NBCROW null-bitmap initialization calls in
SQLServerResultSet.loadColumn()once initialized for the current row.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResultSet.java | Avoid repeated NBCROW null-bitmap initialization calls after the row has been initialized. |
| src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java | Add scratch-buffer String decoding helper; make MONEY/SMALLMONEY decode allocation-free by using long directly. |
| src/main/java/com/microsoft/sqlserver/jdbc/dtv.java | Route small synchronous textual reads through the new scratch-buffer String decode; remove BigInteger intermediates for AE MONEY/SMALLMONEY. |
| src/main/java/com/microsoft/sqlserver/jdbc/DDC.java | Add BigDecimal encoding fast path that avoids BigInteger.toByteArray() when magnitude fits in long. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2991 +/- ##
============================================
+ Coverage 60.19% 60.61% +0.41%
- Complexity 5152 5269 +117
============================================
Files 153 153
Lines 36637 36652 +15
Branches 6721 6725 +4
============================================
+ Hits 22055 22217 +162
+ Misses 10762 10757 -5
+ Partials 3820 3678 -142 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
divang
reviewed
Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a third batch of targeted ResultSet perf optimizations focused on
MONEY/SMALLMONEYdecoding,BigDecimalbyte encoding, synchronousStringreads, and NBCROW null-bitmap initialization. Each change removes throwaway per-cell allocations or redundant per-column work while preserving the exact decoded value and the existing lazy-decoding architecture.On wide-row bulk reads, where every column of every row is decoded, these per-cell temporaries (intermediate
BigIntegerobjects, per-cellbyte[]buffers, and repeated per-column method calls) multiply into billions of short-lived objects, creating young-generation GC pressure that shows up as both lower throughput and periodic latency spikes.Measured result on a 601-column x 1M-row x 200-iteration scan with 400 runs (default + fetchSize=512):
The tail-latency improvement is a notable secondary benefit: fewer allocations slow young-generation growth, reduce minor GC frequency, and shrink the GC-induced latency tail. This PR eliminates 100 % of the
> 60 sand> 65 scatastrophic outliers.Key improvements
MONEY/SMALLMONEYwithout an intermediateBigInteger(both the plaintext and Always Encrypted paths).BigDecimalto wire bytes without an intermediateBigInteger.toByteArray()allocation when the magnitude fits in along.getString()values into the per-reader scratch buffer, avoiding a per-cellbyte[].Changes
1. Allocation-free
MONEY/SMALLMONEYdecode (IOBuffer.java,dtv.java)readMoney()decodes everyMONEY/SMALLMONEYcolumn. The original wrapped the 8-byte (money) or 4-byte (smallmoney) unscaled value in aBigIntegerbefore constructing theBigDecimal. The money/smallmoney unscaled magnitude always fits in along, so build theBigDecimaldirectly withBigDecimal.valueOf(long, scale)and skip the intermediateBigIntegerallocation.The same transformation is applied to the Always Encrypted decode path in
ServerDTVImpl(SMALLMONEYandMONEYcases), replacingnew BigDecimal(BigInteger.valueOf(...), 4)withBigDecimal.valueOf(..., 4).Safety. Value-identical.
BigDecimal.valueOf(long, 4)produces the same compact,long-backedBigDecimalthe original path produced for these magnitudes.2. Allocation-free
BigDecimalbyte encoding (DDC.java)When encoding a
BigDecimalto its TDS wire representation, the original always calledBigInteger.toByteArray()to obtain the magnitude bytes. When the non-negative magnitude fits in along, emit its little-endian bytes directly, matching the minimal two's-complement lengthtoByteArray()would produce, without allocating the intermediatebyte[]. Larger magnitudes fall through to the unchangedBigIntegerpath.Safety. Byte-identical output for the guarded inputs.
numMagBytesis computed to match exactly whatBigInteger.toByteArray().lengthyields for a non-negative magnitude, and larger magnitudes retain the original path.3. Scratch-buffer decode for short synchronous
Stringreads (IOBuffer.java,dtv.java)Building on #2974's inline
getString()fast path, this replaces the per-cellbyte[]allocation with a newreadStringFromBytes()helper that decodes short values from the per-reader scratch buffer. The scratch buffer is only read by theStringconstructor, so it does not escape.Safety. The produced
Stringis identical tonew String(bytes, charset); values larger than the scratch buffer still allocate a right-sizedbyte[]. The existing guards from #2974 (valueLength <= 4000,streamType == NONE,!isAdaptive,jdbcType.isTextual()) are unchanged.4. Skip redundant per-column NBCROW initialization (
SQLServerResultSet.java)loadColumn()calledinitializeNullCompressedColumns()for every column access. For an NBCROW, the null bitmap is read on the first accessed column and a flag is set; subsequent columns re-entered the method only to return immediately. Guard the call with the existingareNullCompressedColumnsInitializedflag to skip the method call entirely once the row is initialized.Safety. Identical behavior. The guard replicates the method's own early-return condition. The flag is reset per row.
Why this is safe to land
BigDecimal.valueOf(long, scale)and the direct byte-emit path produce the same results as the originalBigInteger-based code for the guarded inputs.Stringconstructor withinreadStringFromBytes()and never handed to callers.Benchmark results
Methodology
SELECT *over a 601-column table mixing numeric, string, decimal, and datetime columnsfetchSize=512Combined (default + 512 fetchSize, 400 runs each)
Improvement vs Baseline (negative % = faster)
Observations
> 60 sand> 65 soutliers are eliminated (25 to 0 and 9 to 0), consistent with reduced young-generation GC pressure.Testing
MONEY/SMALLMONEY,DECIMAL/NUMERIC, and string decode paths are already exercised across precision/scale combinations, NBCROW null handling, packet-boundary spanning reads, and Always Encrypted columns.WideRowReadTestsnested class added in Perf: Optimize the ResultSet read path and reduce allocation overhead #2974 (reads a fat table of 10 rows x 601 columns and asserts the exact value of every cell via type-specific getters, covering 20 SQL types including both decimal decode paths) continues to cover the changes here.