Message-ID: From: "vlsi (@vlsi)" To: "pgjdbc/pgjdbc" Date: Thu, 13 Nov 2025 17:21:20 +0000 Subject: [pgjdbc/pgjdbc] PR #3866: perf: optimize PGInterval.getValue() by replacing String.format with StringBuilder List-Id: X-GitHub-Author-Id: 213894 X-GitHub-Author-Login: vlsi X-GitHub-Issue: 3866 X-GitHub-Labels: performance X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-State: merged X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/3866 Content-Type: text/plain; charset=utf-8 TODO: - [x] review the code Replace String.format() and DecimalFormat with manual StringBuilder concatenation in PGInterval.getValue() to improve performance. Fixes #3865 ## benchmarks Apple M1, Java 21, 5 forks, 3 warmup iterations, 5 measurement iterations ### Before ``` Benchmark (inputType) Mode Cnt Score Error Units getValue ZERO avgt 25 1057,606 ± 24,991 ns/op getValue:gc.alloc.rate.norm ZERO avgt 25 3208,007 ± 0,001 B/op getValue RANDOM_SECONDS avgt 25 1173,411 ± 17,457 ns/op getValue:gc.alloc.rate.norm RANDOM_SECONDS avgt 25 3206,408 ± 107,854 B/op getValue RANDOM_DAYS avgt 25 1202,730 ± 38,124 ns/op getValue:gc.alloc.rate.norm RANDOM_DAYS avgt 25 3206,408 ± 100,678 B/op getValue RANDOM_YEARS avgt 25 1200,332 ± 37,579 ns/op getValue:gc.alloc.rate.norm RANDOM_YEARS avgt 25 3123,208 ± 160,827 B/op ``` ### After ``` Benchmark (inputType) Mode Cnt Score Error Units getValue ZERO avgt 25 7,654 ± 0,249 ns/op getValue:gc.alloc.rate.norm ZERO avgt 25 128,000 ± 0,001 B/op getValue RANDOM_SECONDS avgt 25 17,306 ± 0,681 ns/op getValue:gc.alloc.rate.norm RANDOM_SECONDS avgt 25 136,000 ± 0,001 B/op getValue RANDOM_DAYS avgt 25 44,907 ± 18,940 ns/op getValue:gc.alloc.rate.norm RANDOM_DAYS avgt 25 156,800 ± 4,893 B/op getValue RANDOM_YEARS avgt 25 74,583 ± 1,102 ns/op getValue:gc.alloc.rate.norm RANDOM_YEARS avgt 25 182,401 ± 2,446 B/op ``` ## Discarded options ### Estimate capacity for `StringBuilder` The idea is to reserve 11 chars for every non-zero field. The computation could be branchless, so it should be relatively fast. However, it still have noticeable slowdown for small and medium inputs. ```java ### Estimage capacity for `StringBuilder` ```java int estimateCapacity = 11 * ( isNonZero(years) + isNonZero(months) + isNonZero(days) + isNonZero(hours) + isNonZero(minutes) + isNonZero(wholeSeconds) + 1 ); private int isNonZero(int value) { // The logic is as follows: // For zero, the result is zero // For non-zero, either value or -value will have the highest bit set return (value | -value) >>> 31; } ``` ``` Benchmark (inputType) Mode Cnt Score Error Units getValue ZERO avgt 25 24,712 ± 0,494 ns/op getValue:gc.alloc.rate.norm ZERO avgt 25 80,000 ± 0,001 B/op getValue RANDOM_SECONDS avgt 25 23,771 ± 1,428 ns/op getValue:gc.alloc.rate.norm RANDOM_SECONDS avgt 25 96,000 ± 0,001 B/op getValue RANDOM_DAYS avgt 25 47,108 ± 3,814 ns/op getValue:gc.alloc.rate.norm RANDOM_DAYS avgt 25 171,200 ± 7,339 B/op getValue RANDOM_YEARS avgt 25 67,121 ± 0,728 ns/op getValue:gc.alloc.rate.norm RANDOM_YEARS avgt 25 222,400 ± 2,446 B/op ``` ### Compute exact capacity for `StringBuilder` I tried the following to estimate the string builder length exactly, however it results in slower performance overall (less allocations, but slower perf for all cases except ZERO) ```java private int stringSize() { int stringSize = stringSize(wholeSeconds) + 5; if (years != 0) { stringSize += stringSize(years) + 7; } if (months != 0) { stringSize += stringSize(months) + 6; } if (days != 0) { stringSize += stringSize(days) + 6; } if (hours != 0) { stringSize += stringSize(hours) + 7; } if (minutes != 0) { stringSize += stringSize(minutes) + 6; } if (microSeconds != 0) { stringSize += 7; } return stringSize; } private static int stringSize(int x) { int d = 1; // assume negative (need minus sign) if (x >= 0) { d = 0; // positive, no minus sign needed // The reason we prefer negative values is -Integer.MIN_VALUE can't fit into a positive range x = -x; // convert to negative for safe comparison } // Now x is negative, compare against negative thresholds if (x > -10) return 1 + d; if (x > -100) return 2 + d; if (x > -1000) return 3 + d; if (x > -10000) return 4 + d; if (x > -100000) return 5 + d; if (x > -1000000) return 6 + d; if (x > -10000000) return 7 + d; if (x > -100000000) return 8 + d; if (x > -1000000000) return 9 + d; return 10 + d; } ```