- PVSM.RU - https://www.pvsm.ru -
Всем привет, к прошлой статье о наследии StringBuffer [1] в комментариях оставили интересную ссылку [2]. В этой статье есть интересный бенчмарк, который я изменил для придания большей драматичности:
@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, batchSize = 1000)
@Measurement(iterations = 40, time = 1, batchSize = 1000)
public class Chaining {
private String a1 = "111111111111111111111111";
private String a2 = "222222222222222222222222";
private String a3 = "333333333333333333333333";
@Benchmark
public String typicalChaining() {
return new StringBuilder().append(a1).append(a2).append(a3).toString();
}
@Benchmark
public String noChaining() {
StringBuilder sb = new StringBuilder();
sb.append(a1);
sb.append(a2);
sb.append(a3);
return sb.toString();
}
}
Результат:
Benchmark Mode Cnt Score Error Units
Chaining.noChaining thrpt 40 8408.703 ± 214.582 ops/s
Chaining.typicalChaining thrpt 40 35830.907 ± 1277.455 ops/s
Итого, конкатеницая через цепочку вызовов sb.append().append()
в 4 раза быстрее… Автор из статьи выше утверждает, что разница связана с тем, что в случае цепочки вызовов генерируется меньше байткода и, соответственно, он выполняется быстрее.
Ну что ж, давайте проверим.
Гипотезу можно легко проверить без углубления в байт код — создадим типичный UriBuilder [3]:
public class UriBuilder {
private String schema;
private String host;
private String path;
public UriBuilder setSchema(String schema) {
this.schema = schema;
return this;
}
...
@Override
public String toString() {
return schema + "://" + host + path;
}
}
И повторим бенчмарк:
@BenchmarkMode(Mode.Throughput)
@Fork(1)
@State(Scope.Thread)
@Warmup(iterations = 10, time = 1, batchSize = 1000)
@Measurement(iterations = 40, time = 1, batchSize = 1000)
public class UriBuilderChaining {
private String host = "host";
private String schema = "http";
private String path = "/123/123/123";
@Benchmark
public String chaining() {
return new UriBuilder().setSchema(schema).setHost(host).setPath(path).toString();
}
@Benchmark
public String noChaining() {
UriBuilder uriBuilder = new UriBuilder();
uriBuilder.setSchema(schema);
uriBuilder.setHost(host);
uriBuilder.setPath(path);
return uriBuilder.toString();
}
}
Если причина действительно в количестве байткода, то даже на такой тяжеловесной операции как конкатенация строк, мы должны увидеть разницу.
Результат:
Benchmark Mode Cnt Score Error Units
UriBuilderChaining.chaining thrpt 40 35797.519 ± 2051.165 ops/s
UriBuilderChaining.noChaining thrpt 40 36080.534 ± 1962.470 ops/s
Хм… Разница на уровне погрешности. Значит количество байткода тут ни при чем. Так как аномалия проявляется со StringBuilder
и append()
, то наверное это как-то связано с известной JVM опцией +XX:OptimizeStringConcat
. Давайте проверим. Повторим самый первый тест, но с отключенной опцией.
В JMH через аннотации сделать это можно так:
@Fork(value = 1, jvmArgsAppend = "-XX:-OptimizeStringConcat")
Повторяем первый тест:
Benchmark Mode Cnt Score Error Units
Chaining.noChaining thrpt 40 7598.743 ± 554.192 ops/s
Chaining.typicalChaining thrpt 40 7946.422 ± 313.967 ops/s
Бинго!
Так как соединение строк через x + y
довольно частая операция в любом приложении — Hotspot JVM находит new StringBuilder().append(x).append(y).toString()
паттерны в байткоде и заменяет их на оптимизированный машинний код [4], обходясь без создания промежуточных объектов.
К сожалению, эта оптимизация не применяется к sb.append(x); sb.append(y);
. Разница на больших строках может быть на порядок.
Используйте паттерн «цепочка вызовов» (method chaining), где это возможно. Во-первых, в случае StringBuilder
это поможет JIT заоптимизировать конкатенацию строк. Во-вторых, так генерируется меньше байт кода и это действительно может помочь заинлайнить Ваш метод в некоторых случаях.
Вопрос на SO [5]
Связанный доклад [6] с грязными подробностями от Шипилева.
Автор: Дмитрий Думанский
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/257701
Ссылки в тексте:
[1] прошлой статье о наследии StringBuffer: https://habrahabr.ru/post/329956/
[2] интересную ссылку: http://alblue.bandlem.com/2016/04/jmh-stringbuffer-stringbuilder.html
[3] UriBuilder: https://github.com/doom369/java-microbenchmarks/blob/master/src/test/java/cc/microbenchmarks/core/chaining/UriBuilder.java
[4] заменяет их на оптимизированный машинний код: http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/4d9931ebf861/src/share/vm/opto/stringopts.cpp
[5] Вопрос на SO: https://stackoverflow.com/questions/44334233/why-is-the-stringbuilder-chaining-pattern-sb-appendx-appendy-faster-than-reg
[6] Связанный доклад: https://www.youtube.com/watch?v=YgGAUGC9ksk
[7] Источник: https://habrahabr.ru/post/330220/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.