Explicit StringBuilder use is better than implicit one?
Claes Redestad
claes.redestad at oracle.com
Sun Sep 3 12:31:09 UTC 2017
Hi Kedar,
your benchmark seems sound.
I think it's rather the case that the default String concatenation emitted by javac is limited to each call site, thus in a loop like this it is expanded not into one StringBuilder operation, but one for every step in the loop, i.e., the equivalent of:
String str = "";
for (...) {
str = new StringBuilder(str).append(s).toString();
}
return str;
Which means this routine allocates a StringBuilder and a String for each iteration (and likely expands the SB internal array too). So we're benchmarking something doing 112×2 or even 112×3 allocations and copies with something doing two or three. Seems 10x is within reason.
Now, if you rebuild and run your benchmark with java 9, my guess is you will see the cost of this slow case looking a bit better. This is because optimizations to indify string concat which may help the JIT eliminate more of the costly intermediate steps.
Still, might be worth avoiding naive concat loops even then, if performance is really critical.
Thanks!
/Claes
kedar mhaswade <kedar.mhaswade at gmail.com> skrev: (3 september 2017 06:07:21 CEST)
>All,
>
>I have a rather basic question. I wrote a JMH microbenchmark
><https://github.com/kedarmhaswade/jmh-benchmarks/blob/master/src/main/java/org/sample/StringConcatenationBenchmark.java>
>for string concatenation [1]. It just concatenates given strings to
>produce
>a longer string.
>
>When I run it on my Mac [2] using something like: java -jar
>target/benchmarks.jar StringConcatenationBenchmark -wi 5 -i 20 -f 1
>I get an output like:
>
>Benchmark Mode Cnt Score
>Error Units
>StringConcatenationBenchmark.concatBuilder avgt 20 2768.090 ±
>51.711 ns/op
>StringConcatenationBenchmark.concatBuilderNaive avgt 20 4347.083 ±
>31.835 ns/op
>StringConcatenationBenchmark.concatDefault avgt 20 57356.593 ±
>415.278 ns/op
>
>My questions are:
>
>1) Have I written the benchmark [1] reasonably correctly?
>2) I can understand that the concatBuilder method is faster than
>concatBuilderNaive (copying of the buffer is optimal while growing its
>size). But looking superficially at the byte code (and JLS), although
>the
>concatDefault method uses StringBuilder just like concatBuilderNaive,
>it is
>*significantly* slower. Even with a smaller approxTotal (1024 bytes,
>instead of 4096 bytes), this difference remains (although it is
>smaller).
>
>How can we explain this anomaly? Have I written a string concatenation
>routine that is better than what javac generates? (I must be missing
>something).
>
>Regards,
>Kedar
>[1]
>
>package org.sample;
>
>import org.openjdk.jmh.annotations.Benchmark;
>import org.openjdk.jmh.annotations.BenchmarkMode;
>import org.openjdk.jmh.annotations.Mode;
>import org.openjdk.jmh.annotations.OutputTimeUnit;
>import org.openjdk.jmh.annotations.Scope;
>import org.openjdk.jmh.annotations.Setup;
>import org.openjdk.jmh.annotations.State;
>
>import java.util.ArrayList;
>import java.util.List;
>import java.util.UUID;
>import java.util.concurrent.TimeUnit;
>
>/**
> * <p> A string concatenation benchmark based on Effective Java 2e.
>Item 51. </p>
> * <p> To make it more appealing and useful, I have considered writing
>a hypothetical routine that
> * does string concatenation and applies the knowledge of performance
>implications. </p>
> */
>@State(Scope.Thread)
>@BenchmarkMode(Mode.AverageTime)
>@OutputTimeUnit(TimeUnit.NANOSECONDS)
>public class StringConcatenationBenchmark {
>
> int approxTotal;
> List<String> strings;
>
> @Setup
> public void initialize() {
> approxTotal = 1 << 12; // 4096
> // each UUID is 36 ASCII characters long, we have about 112
>such strings to make it to 4096
> int cap = 112;
> strings = new ArrayList<>(cap);
> for (int i = 0; i < cap; i++) {
> boolean a = strings.add(UUID.randomUUID().toString());
> assert a;
> }
> }
>
> @Benchmark
> public String concatBuilder() {
> StringBuilder sb = new StringBuilder(approxTotal);
> for (String s : strings)
> sb = sb.append(s);
> return sb.toString();
> }
>
> @Benchmark
> public String concatBuilderNaive() {
> StringBuilder sb = new StringBuilder(); // discard
>approxTotal, initial cap is 16
> for (String s : strings)
> sb = sb.append(s);
> return sb.toString();
> }
>
> @Benchmark
> public String concatDefault() {
> String str = "";
> for (String s : strings)
> str += s; // IDE warns!
> return str;
> }
>
> public static void main(String[] args) {
> StringConcatenationBenchmark bm = new StringConcatenationBenchmark();
> bm.initialize();
> String str1 = bm.concatBuilder();
> String str2 = bm.concatDefault();
> String str3 = bm.concatBuilderNaive();
> System.out.println(str1.equals(str2));
> System.out.println(str1.equals(str3));
> }
>}
>
>[2]
>
>$> uname -a
>Darwin C02SG11NG8WL 15.6.0 Darwin Kernel Version 15.6.0: Mon Jan 9
>23:07:29 PST 2017; root:xnu-3248.60.11.2.1~1/RELEASE_X86_64 x86_64
>$> java -version
>java version "1.8.0_131"
>Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
>Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
--
Sent from my Android device with K-9 Mail. Please excuse my brevity.
More information about the jmh-dev
mailing list