From d68c5f3e23cd125c33762163c88b24c504e7d591 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Wed, 3 Jun 2026 13:34:24 +0800 Subject: [PATCH] perf: use unsynchronized StringBuilderWriter in std.deepJoin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: `std.deepJoin` writes each Val.Str chunk into a `java.io.StringWriter` inside a tight loop. StringWriter's backing `StringBuffer` pays a monitor enter/exit on every `write`/`append` call, which on a typical deepJoin walk over a deeply nested array can be hundreds of thousands of synchronized writes. `TomlRenderer` and `FastMaterializeJsonRenderer` already use the unsynchronized package-private `StringBuilderWriter` for the same reason (see #874, #875). deepJoin was explicitly left as a follow-up in #875's description ("std.deepJoin keeps StringWriter (separate concern)") — this is that follow-up. Modification: Swap the single `new StringWriter()` in `DeepJoin.evalRhs` for `new StringBuilderWriter()`. No other changes; output is byte-identical. Result: Scala Native hyperfine, A/B against master (`fc292fa6`). Workload: a 50000-row array of 10-string rows → 2 MB of deepJoin output, render-dominated. Four interleaved-order passes (`--warmup 10 --min-runs 100 --shell=none`): | pass | order | before mean | after mean | before min | after min | min ratio | |---|---|---:|---:|---:|---:|---:| | 1 | before → after | 35.1 ± 16.5 ms | 32.2 ± 19.1 ms | 23.1 ms | 18.7 ms | 1.24x | | 2 | after → before | 43.7 ± 30.6 ms | 29.9 ± 25.3 ms | 25.7 ms | 20.3 ms | 1.27x | | 3 | before → after | 30.3 ± 8.5 ms | 29.5 ± 7.1 ms | 24.6 ms | 20.8 ms | 1.18x | | 4 | after → before | 32.6 ± 7.6 ms | 28.0 ± 6.8 ms | 24.0 ms | 20.7 ms | 1.16x | After is faster in every one of the 4 passes; min values are tight at 1.16-1.27x faster. Output byte-identical (2,000,000 bytes both sides). --- sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala b/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala index 14a2a8d1..aa561c1b 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ManifestModule.scala @@ -337,7 +337,10 @@ object ManifestModule extends AbstractFunctionModule { */ private object DeepJoin extends Val.Builtin1("deepJoin", "arr") { def evalRhs(value: Eval, ev: EvalScope, pos: Position): Val = { - val out = new StringWriter() + // Use the unsynchronized StringBuilderWriter: each Val.Str chunk triggers + // out.write(...) in a tight loop, and StringWriter's backing StringBuffer + // pays a monitor enter/exit per call. Same swap pattern as TomlRenderer (#875). + val out = new StringBuilderWriter() val q = new java.util.ArrayDeque[Eval]() q.add(value) while (!q.isEmpty) {