Updating to have different test methods for each representation did remove the difference for the non-ascii String case for the jdk 21+ releases. However, the ascii (latin) strings are still slower with String than StringBuilder.
How does C2 then handle something like StringCharBuffer wrapping a CharSequence for all of it's get operations: https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/nio/StringCharBuffer.java#L88-L97 Which is then used by CharBufferSpliterator https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/nio/CharBufferSpliterator.java And by many CharsetEncoder impls when either source or destination is not backed by array (which would be the case if StringCharBuffer used): https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/sun/nio/cs/UTF_8.java#L517 https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/sun/nio/cs/UnicodeEncoder.java#L81 jdk 17 Benchmark (data) Mode Cnt Score Error Units CharSequenceCharAtBenchmark.testString ascii avgt 3 1429.358 ± 623.424 ns/op CharSequenceCharAtBenchmark.testString non-ascii avgt 3 705.282 ± 233.453 ns/op CharSequenceCharAtBenchmark.testStringBuilder ascii avgt 3 724.138 ± 267.346 ns/op CharSequenceCharAtBenchmark.testStringBuilder non-ascii avgt 3 718.357 ± 864.066 ns/op jdk 21 Benchmark (data) Mode Cnt Score Error Units CharSequenceCharAtBenchmark.testString ascii avgt 3 1087.024 ┬▒ 235.082 ns/op CharSequenceCharAtBenchmark.testString non-ascii avgt 3 687.520 ┬▒ 747.532 ns/op CharSequenceCharAtBenchmark.testStringBuilder ascii avgt 3 672.802 ┬▒ 29.740 ns/op CharSequenceCharAtBenchmark.testStringBuilder non-ascii avgt 3 689.964 ┬▒ 791.175 ns/op jdk 25 Benchmark (data) Mode Cnt Score Error Units CharSequenceCharAtBenchmark.testString ascii avgt 3 1176.057 ┬▒ 1157.979 ns/op CharSequenceCharAtBenchmark.testString non-ascii avgt 3 697.382 ┬▒ 231.144 ns/op CharSequenceCharAtBenchmark.testStringBuilder ascii avgt 3 692.970 ┬▒ 105.112 ns/op CharSequenceCharAtBenchmark.testStringBuilder non-ascii avgt 3 703.178 ┬▒ 446.019 ns/op jdk 26 Benchmark (data) Mode Cnt Score Error Units CharSequenceCharAtBenchmark.testString ascii avgt 3 1132.971 ┬▒ 350.786 ns/op CharSequenceCharAtBenchmark.testString non-ascii avgt 3 688.201 ┬▒ 175.797 ns/op CharSequenceCharAtBenchmark.testStringBuilder ascii avgt 3 704.380 ┬▒ 101.763 ns/op CharSequenceCharAtBenchmark.testStringBuilder non-ascii avgt 3 673.622 ┬▒ 51.462 ns/op @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; private String string; private StringBuilder stringBuilder; @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); } string = sb.toString(); stringBuilder = sb; } @Benchmark public int testString() { String sequence = this.string; int sum = 0; for (int i=0, j=sequence.length(); i<j; ++i) { sum += sequence.charAt(i); } return sum; } @Benchmark public int testStringBuilder() { StringBuilder sequence = this.stringBuilder; int sum = 0; for (int i=0, j=sequence.length(); i<j; ++i) { sum += sequence.charAt(i); } return sum; } } On Mon, Jul 21, 2025 at 1:12 PM Roger Riggs <roger.ri...@oracle.com> wrote: > Hi Brett, > > I'd suggest separate initialization and test methods for the two cases to > get more reliable numbers. > > By using @Trial and using a common field for the test data, I think you > have handicapped C2. > The training runs JMH does to warm up C2 are 'seeing' two different types > for the value of sequence. > Making the test runs independent will remov doubt about interactions due > to the test setup. > > Roger > > On 7/21/25 1:43 PM, Brett Okken wrote: > > > output labeled as StringBuffer but the jmh creates StringBuilder. > > Ugh - sorry about that. But yes - this is about StringBuilder vs String. > > > I would not be surprised that C2 has more optimizations for String than > for StringBuilder. > > If that were true, it would not surprise me. However, these tests show the > opposite. String is /slower/ than StringBuilder. > > On Mon, Jul 21, 2025 at 12:34 PM Roger Riggs <roger.ri...@oracle.com> > wrote: > >> 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; >>>> } >>>> } >>>> >>> >> >