perf: bulk numeric array literal parser bypassing fastparse#904
Closed
He-Pin wants to merge 3 commits into
Closed
Conversation
Motivation: Parsing large numeric array literals like `[76,111,114,...]` goes through fastparse combinators per element — each number traverses the full expression grammar (expr → exprSuffix → atomExpr → number) before resolving. On Scala Native without JIT, this combinator overhead is significant. Modification: Add `tryBulkNumericArray` that detects arrays of pure numeric literals and parses them with a hand-written scanner. Skips the fastparse expression chain entirely for each element. Falls back to regular parsing for arrays containing non-numeric elements, comprehensions, or comments. Result: Native A/B (member benchmark with ~2000-element numeric array): baseline 8.6ms → optimized 6.2ms (-28%). Ratio vs jrsonnet: 2.12x → 1.53x.
Motivation: The bulk numeric array parser (previous commit) used Double.parseDouble(data.substring()) for each element, creating a substring allocation and invoking the full double parser even for simple integers. Modification: - Parse simple integers directly using 4-digits-at-a-time technique (inspired by jsoniter-scala/PR databricks#897), avoiding substring allocation and Double.parseDouble overhead entirely - Use Val.cachedNum for values 0-255, reusing pre-allocated instances instead of creating new Val.Num objects - Float/exponent numbers still fall back to Double.parseDouble Result: Native A/B (member): 7.1ms → 5.9ms (-17.4%). Combined with bulk parser: member gap vs jrsonnet 1.97x → 1.42x. Also improves base64_byte_array: 11.3ms → 10.4ms (-7.9%).
b9bb525 to
b2df550
Compare
…cision loss Motivation: The bulk numeric array parser used Double for integer accumulation, which loses precision for integers beyond 2^53 (e.g., 12345678901234567890 would differ from Double.parseDouble by up to 2048). Modification: - Changed accumulator from Double to Long with overflow detection. - When acc > Long.MaxValue/10000 (or /10), set isSimpleInt=false and fall back to Double.parseDouble for that number. - Convert Long to Double before negation to preserve -0.0 sign bit. - Added regression test for large integer precision. Result: Bulk parser now produces identical results to Double.parseDouble for all integer magnitudes, including beyond 2^53.
Contributor
Author
|
Even the number is ok, but still this kind of optimization is not that generic. |
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.
Motivation
Parsing large numeric array literals like
[76,111,114,...]goes through fastparse combinators per element — each number traverses the full expression grammar (expr -> exprSuffix -> atomExpr -> number) before resolving to a simple literal. On Scala Native without JIT, this combinator chain overhead is significant (~1.8ms parse time for a ~2000-element array).Modification
Commit 1: Bulk numeric array parser
tryBulkNumericArraythat detects arrays of pure numeric literals and parses them with a hand-written scanner, bypassing the fastparse expression chain entirely123abc) to avoid misparseCommit 2: SWAR integer parsing + Val.cachedNum
String.substringallocation +Double.parseDoubleoverheadVal.cachedNumfor values 0-255, reusing pre-allocated instances instead of creating newVal.NumobjectsDouble.parseDouble(substring)Result
JVM JMH (JDK 21, G1GC, -Xmx4G, @fork(1) @WarmUp(1) @measurement(1), 3 runs averaged):
Noise verification (3 runs, Run1 was outlier for all):
Scala Native A/B (Scala Native 0.5.12, macOS arm64, hyperfine --warmup 3):
References