From be312f2139efa316bf6236c75c1604bb39c86490 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 11:56:01 +0000 Subject: [PATCH 1/5] Buffer char reads in Reader instead of per-char dispatch Replace single-char reader.read() loop with bulk read into a char[8192] buffer and scan for newline over the array. Eliminates per-character method dispatch overhead (synchronized in BufferedReader) while preserving resumability semantics. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01761H3KorZV49nqsiWMqJAz --- src/main/java/org/nsvformat/Reader.java | 29 +++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/nsvformat/Reader.java b/src/main/java/org/nsvformat/Reader.java index 7deef26..eac9ef6 100644 --- a/src/main/java/org/nsvformat/Reader.java +++ b/src/main/java/org/nsvformat/Reader.java @@ -13,25 +13,32 @@ public class Reader implements Iterator> { private final List rowBuffer = new ArrayList<>(); private List cachedRow = null; + private char[] buf = new char[8192]; + private int bufPos = 0; + private int bufLen = 0; + public Reader(java.io.Reader reader) { this.reader = reader; } private String tryReadLine() throws IOException { while (true) { - int c = reader.read(); - if (c == -1) { - // Incomplete line at EOF, preserve lineBuffer for next call - return null; + for (int i = bufPos; i < bufLen; i++) { + if (buf[i] == '\n') { + lineBuffer.append(buf, bufPos, i - bufPos); + bufPos = i + 1; + String line = lineBuffer.toString(); + lineBuffer.setLength(0); + return line; + } } - if (c == '\n') { - // Line complete, return - String line = lineBuffer.toString(); - lineBuffer.setLength(0); - return line; + lineBuffer.append(buf, bufPos, bufLen - bufPos); + bufLen = reader.read(buf, 0, buf.length); + bufPos = 0; + if (bufLen == -1) { + bufLen = 0; + return null; } - // Keep reading - lineBuffer.append((char) c); } } From ed22b0ea01f045feace8b761b275f48d7ccbefda Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 11:59:37 +0000 Subject: [PATCH 2/5] Add Reader benchmark comparing per-char vs bulk-scan Standalone benchmark with the old per-char implementation inlined for A/B comparison. Covers varying row counts, column counts, and field lengths. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01761H3KorZV49nqsiWMqJAz --- .../java/org/nsvformat/ReaderBenchmark.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 src/test/java/org/nsvformat/ReaderBenchmark.java diff --git a/src/test/java/org/nsvformat/ReaderBenchmark.java b/src/test/java/org/nsvformat/ReaderBenchmark.java new file mode 100644 index 0000000..2edabf7 --- /dev/null +++ b/src/test/java/org/nsvformat/ReaderBenchmark.java @@ -0,0 +1,152 @@ +package org.nsvformat; + +import java.io.*; +import java.util.*; + +public class ReaderBenchmark { + + static class OldReader implements Iterator> { + private final java.io.Reader reader; + private final StringBuilder lineBuffer = new StringBuilder(); + private final List rowBuffer = new ArrayList<>(); + private List cachedRow = null; + + OldReader(java.io.Reader reader) { this.reader = reader; } + + private String tryReadLine() throws IOException { + while (true) { + int c = reader.read(); + if (c == -1) return null; + if (c == '\n') { + String line = lineBuffer.toString(); + lineBuffer.setLength(0); + return line; + } + lineBuffer.append((char) c); + } + } + + private List tryReadRow() throws IOException { + while (true) { + String line = tryReadLine(); + if (line == null) return null; + if (line.isEmpty()) { + List row = new ArrayList<>(rowBuffer); + rowBuffer.clear(); + return row; + } + rowBuffer.add(Nsv.unescape(line)); + } + } + + @Override public boolean hasNext() { + if (cachedRow == null) { + try { cachedRow = tryReadRow(); } + catch (IOException e) { throw new UncheckedIOException(e); } + } + return cachedRow != null; + } + + @Override public List next() { + if (!hasNext()) throw new NoSuchElementException(); + List result = cachedRow; + cachedRow = null; + return result; + } + } + + static String generateData(int rows, int colsPerRow, int fieldLen) { + StringBuilder sb = new StringBuilder(); + Random rng = new Random(42); + for (int r = 0; r < rows; r++) { + for (int c = 0; c < colsPerRow; c++) { + for (int i = 0; i < fieldLen; i++) { + char ch = (char) ('a' + rng.nextInt(26)); + sb.append(ch); + } + sb.append('\n'); + } + sb.append('\n'); + } + return sb.toString(); + } + + static int drain(Iterator> it) { + int count = 0; + while (it.hasNext()) { it.next(); count++; } + return count; + } + + static long benchOld(String data, int iters) { + for (int i = 0; i < iters / 2; i++) + drain(new OldReader(new StringReader(data))); + + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) + drain(new OldReader(new StringReader(data))); + return System.nanoTime() - t0; + } + + static long benchNew(String data, int iters) { + for (int i = 0; i < iters / 2; i++) + drain(new Reader(new StringReader(data))); + + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) + drain(new Reader(new StringReader(data))); + return System.nanoTime() - t0; + } + + static long benchDecode(String data, int iters) { + for (int i = 0; i < iters / 2; i++) + Nsv.decode(data); + + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) + Nsv.decode(data); + return System.nanoTime() - t0; + } + + static long benchOldBuffered(String data, int iters) { + for (int i = 0; i < iters / 2; i++) + drain(new OldReader(new BufferedReader(new StringReader(data)))); + + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) + drain(new OldReader(new BufferedReader(new StringReader(data)))); + return System.nanoTime() - t0; + } + + public static void main(String[] args) { + int[][] scenarios = { + // rows, cols, fieldLen, iters + {100, 5, 10, 5000}, + {100, 5, 200, 2000}, + {1000, 10, 50, 500}, + {10000, 5, 20, 100}, + }; + + System.out.printf("%-35s %12s %12s %12s %12s %8s%n", + "scenario", "old(raw)", "old(buf)", "new", "decode", "speedup"); + System.out.println("-".repeat(95)); + + for (int[] s : scenarios) { + int rows = s[0], cols = s[1], flen = s[2], iters = s[3]; + String data = generateData(rows, cols, flen); + String label = rows + "r x " + cols + "c x " + flen + "ch (" + (data.length()/1024) + "KB)"; + + long tOldRaw = benchOld(data, iters); + long tOldBuf = benchOldBuffered(data, iters); + long tNew = benchNew(data, iters); + long tDecode = benchDecode(data, iters); + + double msOldRaw = tOldRaw / 1e6; + double msOldBuf = tOldBuf / 1e6; + double msNew = tNew / 1e6; + double msDecode = tDecode / 1e6; + + System.out.printf("%-35s %10.1f ms %10.1f ms %10.1f ms %10.1f ms %7.2fx%n", + label, msOldRaw, msOldBuf, msNew, msDecode, msOldBuf / msNew); + } + } +} From ff64657a07179f9a9991820c8bf8db7015546cf1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 12:02:58 +0000 Subject: [PATCH 3/5] Use String.indexOf to scan for newlines in decode Replace per-char charAt loop with indexOf('\n', start), a HotSpot intrinsic that scans via SIMD. 1.4x-6.5x speedup on decode, scaling with field length. Update benchmark to cover decode path. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01761H3KorZV49nqsiWMqJAz --- src/main/java/org/nsvformat/Nsv.java | 18 +++--- .../java/org/nsvformat/ReaderBenchmark.java | 64 ++++++++++++++++--- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/nsvformat/Nsv.java b/src/main/java/org/nsvformat/Nsv.java index 32d9530..ef090c5 100644 --- a/src/main/java/org/nsvformat/Nsv.java +++ b/src/main/java/org/nsvformat/Nsv.java @@ -11,17 +11,15 @@ public static List> decode(String s) { List row = new ArrayList<>(); int start = 0; - for (int pos = 0; pos < s.length(); pos++) { - char c = s.charAt(pos); - if (c == '\n') { - if (pos - start >= 1) { - row.add(unescape(s.substring(start, pos))); - } else { - data.add(row); - row = new ArrayList<>(); - } - start = pos + 1; + int pos; + while ((pos = s.indexOf('\n', start)) >= 0) { + if (pos > start) { + row.add(unescape(s.substring(start, pos))); + } else { + data.add(row); + row = new ArrayList<>(); } + start = pos + 1; } if (start < s.length()) { diff --git a/src/test/java/org/nsvformat/ReaderBenchmark.java b/src/test/java/org/nsvformat/ReaderBenchmark.java index 2edabf7..ea115df 100644 --- a/src/test/java/org/nsvformat/ReaderBenchmark.java +++ b/src/test/java/org/nsvformat/ReaderBenchmark.java @@ -97,6 +97,37 @@ static long benchNew(String data, int iters) { return System.nanoTime() - t0; } + static List> oldDecode(String s) { + List> data = new ArrayList<>(); + List row = new ArrayList<>(); + int start = 0; + for (int pos = 0; pos < s.length(); pos++) { + char c = s.charAt(pos); + if (c == '\n') { + if (pos - start >= 1) { + row.add(Nsv.unescape(s.substring(start, pos))); + } else { + data.add(row); + row = new ArrayList<>(); + } + start = pos + 1; + } + } + if (start < s.length()) row.add(Nsv.unescape(s.substring(start))); + if (!row.isEmpty()) data.add(row); + return data; + } + + static long benchOldDecode(String data, int iters) { + for (int i = 0; i < iters / 2; i++) + oldDecode(data); + + long t0 = System.nanoTime(); + for (int i = 0; i < iters; i++) + oldDecode(data); + return System.nanoTime() - t0; + } + static long benchDecode(String data, int iters) { for (int i = 0; i < iters / 2; i++) Nsv.decode(data); @@ -126,9 +157,10 @@ public static void main(String[] args) { {10000, 5, 20, 100}, }; - System.out.printf("%-35s %12s %12s %12s %12s %8s%n", - "scenario", "old(raw)", "old(buf)", "new", "decode", "speedup"); - System.out.println("-".repeat(95)); + System.out.println("=== Reader: old per-char vs new bulk-scan ==="); + System.out.printf("%-35s %12s %12s %12s %8s%n", + "scenario", "old(raw)", "old(buf)", "new", "speedup"); + System.out.println("-".repeat(83)); for (int[] s : scenarios) { int rows = s[0], cols = s[1], flen = s[2], iters = s[3]; @@ -138,15 +170,27 @@ public static void main(String[] args) { long tOldRaw = benchOld(data, iters); long tOldBuf = benchOldBuffered(data, iters); long tNew = benchNew(data, iters); - long tDecode = benchDecode(data, iters); - double msOldRaw = tOldRaw / 1e6; - double msOldBuf = tOldBuf / 1e6; - double msNew = tNew / 1e6; - double msDecode = tDecode / 1e6; + System.out.printf("%-35s %10.1f ms %10.1f ms %10.1f ms %7.2fx%n", + label, tOldRaw/1e6, tOldBuf/1e6, tNew/1e6, (double)tOldBuf / tNew); + } + + System.out.println(); + System.out.println("=== Decode: old charAt vs new indexOf ==="); + System.out.printf("%-35s %12s %12s %8s%n", + "scenario", "old", "new", "speedup"); + System.out.println("-".repeat(71)); + + for (int[] s : scenarios) { + int rows = s[0], cols = s[1], flen = s[2], iters = s[3]; + String data = generateData(rows, cols, flen); + String label = rows + "r x " + cols + "c x " + flen + "ch (" + (data.length()/1024) + "KB)"; + + long tOld = benchOldDecode(data, iters); + long tNew = benchDecode(data, iters); - System.out.printf("%-35s %10.1f ms %10.1f ms %10.1f ms %10.1f ms %7.2fx%n", - label, msOldRaw, msOldBuf, msNew, msDecode, msOldBuf / msNew); + System.out.printf("%-35s %10.1f ms %10.1f ms %7.2fx%n", + label, tOld/1e6, tNew/1e6, (double)tOld / tNew); } } } From 3a33dbbebc825ea2b259b5ece82462e5d118c9e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 12:13:54 +0000 Subject: [PATCH 4/5] Remove benchmark file from tree Retained in branch history for reference. Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01761H3KorZV49nqsiWMqJAz --- .../java/org/nsvformat/ReaderBenchmark.java | 196 ------------------ 1 file changed, 196 deletions(-) delete mode 100644 src/test/java/org/nsvformat/ReaderBenchmark.java diff --git a/src/test/java/org/nsvformat/ReaderBenchmark.java b/src/test/java/org/nsvformat/ReaderBenchmark.java deleted file mode 100644 index ea115df..0000000 --- a/src/test/java/org/nsvformat/ReaderBenchmark.java +++ /dev/null @@ -1,196 +0,0 @@ -package org.nsvformat; - -import java.io.*; -import java.util.*; - -public class ReaderBenchmark { - - static class OldReader implements Iterator> { - private final java.io.Reader reader; - private final StringBuilder lineBuffer = new StringBuilder(); - private final List rowBuffer = new ArrayList<>(); - private List cachedRow = null; - - OldReader(java.io.Reader reader) { this.reader = reader; } - - private String tryReadLine() throws IOException { - while (true) { - int c = reader.read(); - if (c == -1) return null; - if (c == '\n') { - String line = lineBuffer.toString(); - lineBuffer.setLength(0); - return line; - } - lineBuffer.append((char) c); - } - } - - private List tryReadRow() throws IOException { - while (true) { - String line = tryReadLine(); - if (line == null) return null; - if (line.isEmpty()) { - List row = new ArrayList<>(rowBuffer); - rowBuffer.clear(); - return row; - } - rowBuffer.add(Nsv.unescape(line)); - } - } - - @Override public boolean hasNext() { - if (cachedRow == null) { - try { cachedRow = tryReadRow(); } - catch (IOException e) { throw new UncheckedIOException(e); } - } - return cachedRow != null; - } - - @Override public List next() { - if (!hasNext()) throw new NoSuchElementException(); - List result = cachedRow; - cachedRow = null; - return result; - } - } - - static String generateData(int rows, int colsPerRow, int fieldLen) { - StringBuilder sb = new StringBuilder(); - Random rng = new Random(42); - for (int r = 0; r < rows; r++) { - for (int c = 0; c < colsPerRow; c++) { - for (int i = 0; i < fieldLen; i++) { - char ch = (char) ('a' + rng.nextInt(26)); - sb.append(ch); - } - sb.append('\n'); - } - sb.append('\n'); - } - return sb.toString(); - } - - static int drain(Iterator> it) { - int count = 0; - while (it.hasNext()) { it.next(); count++; } - return count; - } - - static long benchOld(String data, int iters) { - for (int i = 0; i < iters / 2; i++) - drain(new OldReader(new StringReader(data))); - - long t0 = System.nanoTime(); - for (int i = 0; i < iters; i++) - drain(new OldReader(new StringReader(data))); - return System.nanoTime() - t0; - } - - static long benchNew(String data, int iters) { - for (int i = 0; i < iters / 2; i++) - drain(new Reader(new StringReader(data))); - - long t0 = System.nanoTime(); - for (int i = 0; i < iters; i++) - drain(new Reader(new StringReader(data))); - return System.nanoTime() - t0; - } - - static List> oldDecode(String s) { - List> data = new ArrayList<>(); - List row = new ArrayList<>(); - int start = 0; - for (int pos = 0; pos < s.length(); pos++) { - char c = s.charAt(pos); - if (c == '\n') { - if (pos - start >= 1) { - row.add(Nsv.unescape(s.substring(start, pos))); - } else { - data.add(row); - row = new ArrayList<>(); - } - start = pos + 1; - } - } - if (start < s.length()) row.add(Nsv.unescape(s.substring(start))); - if (!row.isEmpty()) data.add(row); - return data; - } - - static long benchOldDecode(String data, int iters) { - for (int i = 0; i < iters / 2; i++) - oldDecode(data); - - long t0 = System.nanoTime(); - for (int i = 0; i < iters; i++) - oldDecode(data); - return System.nanoTime() - t0; - } - - static long benchDecode(String data, int iters) { - for (int i = 0; i < iters / 2; i++) - Nsv.decode(data); - - long t0 = System.nanoTime(); - for (int i = 0; i < iters; i++) - Nsv.decode(data); - return System.nanoTime() - t0; - } - - static long benchOldBuffered(String data, int iters) { - for (int i = 0; i < iters / 2; i++) - drain(new OldReader(new BufferedReader(new StringReader(data)))); - - long t0 = System.nanoTime(); - for (int i = 0; i < iters; i++) - drain(new OldReader(new BufferedReader(new StringReader(data)))); - return System.nanoTime() - t0; - } - - public static void main(String[] args) { - int[][] scenarios = { - // rows, cols, fieldLen, iters - {100, 5, 10, 5000}, - {100, 5, 200, 2000}, - {1000, 10, 50, 500}, - {10000, 5, 20, 100}, - }; - - System.out.println("=== Reader: old per-char vs new bulk-scan ==="); - System.out.printf("%-35s %12s %12s %12s %8s%n", - "scenario", "old(raw)", "old(buf)", "new", "speedup"); - System.out.println("-".repeat(83)); - - for (int[] s : scenarios) { - int rows = s[0], cols = s[1], flen = s[2], iters = s[3]; - String data = generateData(rows, cols, flen); - String label = rows + "r x " + cols + "c x " + flen + "ch (" + (data.length()/1024) + "KB)"; - - long tOldRaw = benchOld(data, iters); - long tOldBuf = benchOldBuffered(data, iters); - long tNew = benchNew(data, iters); - - System.out.printf("%-35s %10.1f ms %10.1f ms %10.1f ms %7.2fx%n", - label, tOldRaw/1e6, tOldBuf/1e6, tNew/1e6, (double)tOldBuf / tNew); - } - - System.out.println(); - System.out.println("=== Decode: old charAt vs new indexOf ==="); - System.out.printf("%-35s %12s %12s %8s%n", - "scenario", "old", "new", "speedup"); - System.out.println("-".repeat(71)); - - for (int[] s : scenarios) { - int rows = s[0], cols = s[1], flen = s[2], iters = s[3]; - String data = generateData(rows, cols, flen); - String label = rows + "r x " + cols + "c x " + flen + "ch (" + (data.length()/1024) + "KB)"; - - long tOld = benchOldDecode(data, iters); - long tNew = benchDecode(data, iters); - - System.out.printf("%-35s %10.1f ms %10.1f ms %7.2fx%n", - label, tOld/1e6, tNew/1e6, (double)tOld / tNew); - } - } -} From afa05efc62288ce9a567aa9437d82e09a3517a83 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 12:17:59 +0000 Subject: [PATCH 5/5] Restore comments in Reader.tryReadLine Co-Authored-By: Claude Opus 4.6 Claude-Session: https://claude.ai/code/session_01761H3KorZV49nqsiWMqJAz --- src/main/java/org/nsvformat/Reader.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/nsvformat/Reader.java b/src/main/java/org/nsvformat/Reader.java index eac9ef6..fa48076 100644 --- a/src/main/java/org/nsvformat/Reader.java +++ b/src/main/java/org/nsvformat/Reader.java @@ -25,6 +25,7 @@ private String tryReadLine() throws IOException { while (true) { for (int i = bufPos; i < bufLen; i++) { if (buf[i] == '\n') { + // Line complete, return lineBuffer.append(buf, bufPos, i - bufPos); bufPos = i + 1; String line = lineBuffer.toString(); @@ -32,10 +33,12 @@ private String tryReadLine() throws IOException { return line; } } + // Keep reading lineBuffer.append(buf, bufPos, bufLen - bufPos); bufLen = reader.read(buf, 0, buf.length); bufPos = 0; if (bufLen == -1) { + // Incomplete line at EOF, preserve lineBuffer for next call bufLen = 0; return null; }