pgjdbc/pgjdbc GitHub issues and pull requests (mirror)  
help / color / mirror / Atom feed
From: vlsi (@vlsi) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #3866: perf: optimize PGInterval.getValue() by replacing String.format with StringBuilder
Date: Thu, 13 Nov 2025 17:21:20 +0000
Message-ID: <[email protected]> (raw)

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;
  }
```



view thread (4+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: github://pgjdbc/pgjdbc
  Cc: [email protected], [email protected]
  Subject: Re: [pgjdbc/pgjdbc] PR #3866: perf: optimize PGInterval.getValue() by replacing String.format with StringBuilder
  In-Reply-To: <<[email protected]>>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox