Hi Brett,

The labeling of the output is confusing, the test output labeled as StringBuffer but the jmh creates StringBuilder. (StringBuffer methods are all synchronized and could explain why they are slower).

Also, I would not be surprised that C2 has more optimizations for String than for StringBuilder.

Regards, Roger

On 7/19/25 6:09 PM, Brett Okken wrote:
Making sequence a local variable does improve things (especially for ascii), but a substantial difference remains. It appears that the performance difference for ascii goes all the way back to jdk 11. The difference for non-ascii showed up in jdk 21. I wonder if this is related to the index checks?

jdk 11

Benchmark  (data)      (source)  Mode  Cnt     Score  Error  Units
test        ascii        String  avgt    3  1137.348 ± 12.835  ns/op
test        ascii  StringBuffer  avgt    3   712.874 ±  509.320  ns/op
test    non-ascii        String  avgt    3   668.657 ±  246.550  ns/op
test    non-ascii  StringBuffer  avgt    3   897.344 ± 4353.414  ns/op


jdk 17
Benchmark  (data)      (source)  Mode  Cnt     Score  Error  Units
test        ascii        String  avgt    3  1321.497 ± 2107.466  ns/op
test        ascii  StringBuffer  avgt    3   715.936 ±  412.189  ns/op
test    non-ascii        String  avgt    3   722.986 ±  443.389  ns/op
test    non-ascii  StringBuffer  avgt    3   722.787 ±  771.816  ns/op


jdk 21
Benchmark  (data)      (source)  Mode  Cnt     Score Error  Units
test        ascii        String  avgt    3  1150.301 ┬▒ 918.549  ns/op
test        ascii  StringBuffer  avgt    3   713.183 ┬▒ 543.850  ns/op
test    non-ascii        String  avgt    3  4642.667 ┬▒ 11481.029  ns/op
test    non-ascii  StringBuffer  avgt    3   728.027 ┬▒ 936.521  ns/op


jdk 25
Benchmark  (data)      (source)  Mode  Cnt     Score  Error  Units
test        ascii        String  avgt    3  1184.513 ┬▒ 2057.498  ns/op
test        ascii  StringBuffer  avgt    3   786.611 ┬▒  411.657  ns/op
test    non-ascii        String  avgt    3  4197.585 ┬▒ 2761.388  ns/op
test    non-ascii  StringBuffer  avgt    3   716.375 ┬▒  815.349  ns/op


jdk 26
Benchmark  (data)      (source)  Mode  Cnt     Score     Error  Units
test        ascii        String  avgt    3  1107.207 ┬▒ 423.072  ns/op
test        ascii  StringBuffer  avgt    3   742.780 ┬▒ 178.890  ns/op
test    non-ascii        String  avgt    3  4043.914 ┬▒ 498.439  ns/op
test    non-ascii  StringBuffer  avgt    3   712.535 ┬▒ 583.255  ns/op


On Sat, Jul 19, 2025 at 4:17 PM Chen Liang <liangchenb...@gmail.com> wrote:

    Without looking at C2 IRs, I think there are a few potential
    culprits we can look into:
    1. JDK-8351000 and JDK-8351443 updated StringBuilder
    2. Sequence field is read in the loop; I wonder if making it an
    explicit immutable local variable changes anything here.

    On Sat, Jul 19, 2025 at 2:34 PM Brett Okken
    <brett.okken...@gmail.com> wrote:

        I was looking at the performance of StringCharBuffer for various
        backing CharSequence types and was surprised to see a significant
        performance difference between String and StringBuffer. I wrote a
        small jmh which shows that the String implementation of charAt is
        significantly slower than StringBuilder. Is this expected?

        Benchmark                            (data) (source)  Mode  Cnt
          Score       Error  Units
        CharSequenceCharAtBenchmark.test      ascii String  avgt    3
        2537.311 ┬▒  8952.197  ns/op
        CharSequenceCharAtBenchmark.test      ascii StringBuffer 
        avgt    3
        852.004 ┬▒  2532.958  ns/op
        CharSequenceCharAtBenchmark.test  non-ascii String  avgt    3
        5115.381 ┬▒ 13822.592  ns/op
        CharSequenceCharAtBenchmark.test  non-ascii StringBuffer 
        avgt    3
        836.230 ┬▒  1154.191  ns/op



        @Measurement(iterations = 3, time = 5, timeUnit =
        TimeUnit.SECONDS)
        @Warmup(iterations = 2, time = 7, timeUnit = TimeUnit.SECONDS)
        @BenchmarkMode(Mode.AverageTime)
        @OutputTimeUnit(TimeUnit.NANOSECONDS)
        @State(Scope.Benchmark)
        @Fork(value = 1, jvmArgsPrepend = {"-Xms512M", "-Xmx512M"})
        public class CharSequenceCharAtBenchmark {

            @Param(value = {"ascii", "non-ascii"})
            public String data;

            @Param(value = {"String", "StringBuffer"})
            public String source;

            private CharSequence sequence;

            @Setup(Level.Trial)
            public void setup() throws Exception {
                StringBuilder sb = new StringBuilder(3152);
                for (int i=0; i<3152; ++i) {
                    char c = (char) i;
                    if ("ascii".equals(data)) {
                        c = (char) (i & 0x7f);
                    }
                    sb.append(c);
                }

                switch(source) {
                    case "String":
                        sequence = sb.toString();
                        break;
                    case "StringBuffer":
                        sequence = sb;
                        break;
                    default:
                        throw new IllegalArgumentException(source);
                }
            }

            @Benchmark
            public int test() {
                int sum = 0;
                for (int i=0, j=sequence.length(); i<j; ++i) {
                    sum += sequence.charAt(i);
                }
                return sum;
            }
        }

Reply via email to