Недавно я столкнулся с проблемой касательно конкатенации строк. Этот тест обобщает это:
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class BrokenConcatenationBenchmark {
@Benchmark
public String slow(Data data) {
final Class<? extends Data> clazz = data.clazz;
return "class " + clazz.getName();
}
@Benchmark
public String fast(Data data) {
final Class<? extends Data> clazz = data.clazz;
final String clazzName = clazz.getName();
return "class " + clazzName;
}
@State(Scope.Thread)
public static class Data {
final Class<? extends Data> clazz = getClass();
@Setup
public void setup() {
//explicitly load name via native method Class.getName0()
clazz.getName();
}
}
}
На JDK 1.8.0_222 (64-битная виртуальная машина OpenJDK, 25.222-b10) я получил следующие результаты:
Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 22,253 ± 0,962 ns/op
BrokenConcatenationBenchmark.fast:·gc.alloc.rate avgt 25 9824,603 ± 400,088 MB/sec
BrokenConcatenationBenchmark.fast:·gc.alloc.rate.norm avgt 25 240,000 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space avgt 25 9824,162 ± 397,745 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Eden_Space.norm avgt 25 239,994 ± 0,522 B/op
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space avgt 25 0,040 ± 0,011 MB/sec
BrokenConcatenationBenchmark.fast:·gc.churn.PS_Survivor_Space.norm avgt 25 0,001 ± 0,001 B/op
BrokenConcatenationBenchmark.fast:·gc.count avgt 25 3798,000 counts
BrokenConcatenationBenchmark.fast:·gc.time avgt 25 2241,000 ms
BrokenConcatenationBenchmark.slow avgt 25 54,316 ± 1,340 ns/op
BrokenConcatenationBenchmark.slow:·gc.alloc.rate avgt 25 8435,703 ± 198,587 MB/sec
BrokenConcatenationBenchmark.slow:·gc.alloc.rate.norm avgt 25 504,000 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space avgt 25 8434,983 ± 198,966 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Eden_Space.norm avgt 25 503,958 ± 1,000 B/op
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space avgt 25 0,127 ± 0,011 MB/sec
BrokenConcatenationBenchmark.slow:·gc.churn.PS_Survivor_Space.norm avgt 25 0,008 ± 0,001 B/op
BrokenConcatenationBenchmark.slow:·gc.count avgt 25 3789,000 counts
BrokenConcatenationBenchmark.slow:·gc.time avgt 25 2245,000 ms
Это похоже на проблему, подобную JDK-8043677 , где выражение с побочным эффектом нарушает оптимизацию новой StringBuilder.append().append().toString()
цепочки. Но сам по Class.getName()
себе код , похоже, не имеет побочных эффектов:
private transient String name;
public String getName() {
String name = this.name;
if (name == null) {
this.name = name = this.getName0();
}
return name;
}
private native String getName0();
Единственная подозрительная вещь здесь - это вызов нативного метода, который на самом деле происходит только один раз, и его результат кэшируется в поле класса. В моем тесте я явно кэшировал его в методе установки.
Я ожидал, что предиктор ветвей выяснит, что при каждом вызове эталонного значения фактическое значение this.name никогда не будет нулевым, и оптимизирует все выражение.
Однако пока для BrokenConcatenationBenchmark.fast()
меня есть это:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::fast (30 bytes) force inline by CompileCommand
@ 6 java.lang.Class::getName (18 bytes) inline (hot)
@ 14 java.lang.Class::initClassName (0 bytes) native method
@ 14 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 19 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 23 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 26 java.lang.StringBuilder::toString (35 bytes) inline (hot)
т. е. компилятор может встроить все, потому что BrokenConcatenationBenchmark.slow()
он другой:
@ 19 tsypanov.strings.benchmark.concatenation.BrokenConcatenationBenchmark::slow (28 bytes) force inline by CompilerOracle
@ 9 java.lang.StringBuilder::<init> (7 bytes) inline (hot)
@ 3 java.lang.AbstractStringBuilder::<init> (12 bytes) inline (hot)
@ 1 java.lang.Object::<init> (1 bytes) inline (hot)
@ 14 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 18 java.lang.Class::getName (21 bytes) inline (hot)
@ 11 java.lang.Class::getName0 (0 bytes) native method
@ 21 java.lang.StringBuilder::append (8 bytes) inline (hot)
@ 2 java.lang.AbstractStringBuilder::append (50 bytes) inline (hot)
@ 10 java.lang.String::length (6 bytes) inline (hot)
@ 21 java.lang.AbstractStringBuilder::ensureCapacityInternal (27 bytes) inline (hot)
@ 17 java.lang.AbstractStringBuilder::newCapacity (39 bytes) inline (hot)
@ 20 java.util.Arrays::copyOf (19 bytes) inline (hot)
@ 11 java.lang.Math::min (11 bytes) (intrinsic)
@ 14 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 35 java.lang.String::getChars (62 bytes) inline (hot)
@ 58 java.lang.System::arraycopy (0 bytes) (intrinsic)
@ 24 java.lang.StringBuilder::toString (17 bytes) inline (hot)
Таким образом, вопрос в том, является ли это подходящим поведением ошибки JVM или ошибки компилятора?
Я задаю вопрос, потому что некоторые из проектов все еще используют Java 8, и если это не будет исправлено ни в одном из обновлений выпуска, то для меня будет разумным поднимать вызовы Class.getName()
вручную из горячих точек.
PS На последних JDK (11, 13, 14-eap) проблема не воспроизводится.
источник
this.name
.Class.getName()
и вsetUp()
методе, а не в теле тестируемого.Ответы:
HotSpot JVM собирает статистику выполнения по байт-коду. Если один и тот же код выполняется в разных контекстах, профиль результатов будет агрегировать статистику по всем контекстам. Этот эффект известен как загрязнение профиля .
Class.getName()
очевидно вызывается не только из вашего кода теста. Перед тем, как JIT начинает компилировать тест, он уже знает, что следующее условие inClass.getName()
было выполнено несколько раз:По крайней мере, достаточно раз, чтобы рассматривать эту ветвь статистически важно. Таким образом, JIT не исключил эту ветку из компиляции и, следовательно, не смог оптимизировать concat строки из-за возможного побочного эффекта.
Это даже не должно быть вызовом нативного метода. Простое назначение поля также считается побочным эффектом.
Вот пример того, как загрязнение профиля может нанести вред дальнейшей оптимизации.
Это в основном модифицированная версия вашего теста, имитирующая загрязнение
getName()
профиля. В зависимости от количества предварительныхgetName()
вызовов свежего объекта, дальнейшая производительность конкатенации строк может существенно отличаться:Больше примеров загрязнения профилей »
Я не могу назвать это ни ошибкой, ни "соответствующим поведением". Именно так в HotSpot реализована динамическая адаптивная компиляция.
источник
Немного несвязанный, но после Java 9 и JEP 280: Indify String Concatenation теперь конкатенация строк выполняется с использованием,
invokedynamic
а не сStringBuilder
. В этой статье показаны различия в байт-коде между Java 8 и Java 9.Если повторный запуск эталонного теста на более новой версии Java не показывает проблему, то, скорее всего, ошибки нет,
javac
поскольку компилятор теперь использует новый механизм. Не уверен, выгодно ли погружаться в поведение Java 8, если в более новых версиях есть такое существенное изменение.источник
javac
.javac
генерирует байт-код и не выполняет сложных оптимизаций. Я выполнил один и тот же тест с-XX:TieredStopAtLevel=1
полученным результатом:Benchmark Mode Cnt Score Error Units
BrokenConcatenationBenchmark.fast avgt 25 74,677 ? 2,961 ns/op
BrokenConcatenationBenchmark.slow avgt 25 69,316 ? 1,239 ns/op
поэтому, когда мы не оптимизируем много, оба метода дают одинаковые результаты, проблема проявляется только тогда, когда код компилируется в C2.invokedynamic
только указывает среде выполнения выбрать способ конкатенации, и 5 из 6 стратегий (включая стандартную) по-прежнему используютсяStringBuilder
.StringConcatFactory.Strategy
enum?