Всем привет, к прошлой статье о наследии StringBuffer в комментариях оставили интересную ссылку. В этой статье есть интересный бенчмарк, который я изменил для придания большей драматичности:
Результат:
Итого, конкатеницая через цепочку вызовов
Ну что ж, давайте проверим.
Гипотезу можно легко проверить без углубления в байт код — создадим типичный UriBuilder:
И повторим бенчмарк:
Если причина действительно в количестве байткода, то даже на такой тяжеловесной операции как конкатенация строк, мы должны увидеть разницу.
Результат:
Хм… Разница на уровне погрешности. Значит количество байткода тут ни при чем. Так как аномалия проявляется со
В JMH через аннотации сделать это можно так:
Повторяем первый тест:
Бинго!
Так как соединение строк через
К сожалению, эта оптимизация не применяется к
Используйте паттерн «цепочка вызовов» (method chaining), где это возможно. Во-первых, в случае
Вопрос на SO
Связанный доклад с грязными подробностями от Шипилева.
@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:
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() паттерны в байткоде и заменяет их на оптимизированный машинний код, обходясь без создания промежуточных объектов. К сожалению, эта оптимизация не применяется к
sb.append(x); sb.append(y);. Разница на больших строках может быть на порядок.Выводы
Используйте паттерн «цепочка вызовов» (method chaining), где это возможно. Во-первых, в случае
StringBuilder это поможет JIT заоптимизировать конкатенацию строк. Во-вторых, так генерируется меньше байт кода и это действительно может помочь заинлайнить Ваш метод в некоторых случаях.Вопрос на SO
Связанный доклад с грязными подробностями от Шипилева.
