Ostatnio natknąłem się na problem dotyczący konkatenacji ciągów. Ten test porównawczy podsumowuje:
@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();
}
}
}
Na JDK 1.8.0_222 (VM 64-bitowego serwera OpenJDK, 25.222-b10) mam następujące wyniki:
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
Wygląda to na problem podobny do JDK-8043677 , w którym wyrażenie wywołujące efekt uboczny przerywa optymalizację nowego StringBuilder.append().append().toString()
łańcucha. Ale sam kod Class.getName()
nie wydaje się mieć żadnych skutków ubocznych:
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();
Jedyną podejrzaną rzeczą jest tutaj wywołanie metody natywnej, które dzieje się tylko raz, a jej wynik jest buforowany w polu klasy. W moim teście wyraźnie zapisałem to w pamięci podręcznej podczas instalacji.
Oczekiwałem, że predyktor gałęzi zorientuje się, że przy każdym wywołaniu testu porównawczego rzeczywista wartość this.name nigdy nie jest zerowa i optymalizuje całe wyrażenie.
Jednak dla BrokenConcatenationBenchmark.fast()
mam to:
@ 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)
tzn. kompilator jest w stanie wstawić wszystko, BrokenConcatenationBenchmark.slow()
ponieważ jest inaczej:
@ 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)
Pytanie brzmi więc, czy jest to właściwe zachowanie błędu JVM czy kompilatora?
Zadaję to pytanie, ponieważ niektóre projekty nadal używają języka Java 8 i jeśli nie zostanie to naprawione w żadnej z aktualizacji wydania, rozsądne jest, aby Class.getName()
ręcznie wywoływać połączenia z gorących punktów.
PS W najnowszych pakietach JDK (11, 13, 14-eap) problem nie jest odtwarzany.
źródło
this.name
.Class.getName()
i wsetUp()
metodzie, a nie w treści testu porównawczego.Odpowiedzi:
HotSpot JVM zbiera statystyki wykonania według kodu bajtowego. Jeśli ten sam kod jest uruchamiany w różnych kontekstach, profil wynikowy agreguje statystyki ze wszystkich kontekstów. Ten efekt jest znany jako zanieczyszczenie profilu .
Class.getName()
jest oczywiście wywoływane nie tylko z kodu testu porównawczego. Zanim JIT rozpocznie kompilację testu porównawczego, wie już, że następujący warunekClass.getName()
został spełniony wiele razy:Przynajmniej tyle razy, aby potraktować tę gałąź statystycznie ważną. Tak więc JIT nie wykluczył tej gałęzi z kompilacji, a zatem nie mógł zoptymalizować konkatku ciągu ze względu na możliwy efekt uboczny.
Nie musi to być nawet natywne wywołanie metody. Zwykłe przypisanie do pola jest również uważane za efekt uboczny.
Oto przykład, w jaki sposób zanieczyszczenie profilu może zaszkodzić dalszym optymalizacjom.
Jest to w zasadzie zmodyfikowana wersja twojego testu porównawczego, która symuluje zanieczyszczenie
getName()
profilu. W zależności od liczby wstępnychgetName()
wywołań nowego obiektu dalsza wydajność konkatenacji łańcuchów może się znacznie różnić:Więcej przykładów zanieczyszczenia profilu »
Nie mogę tego nazwać ani błędem, ani „odpowiednim zachowaniem”. Tak właśnie implementowana jest dynamiczna kompilacja adaptacyjna w HotSpot.
źródło
Nieznacznie niezwiązane, ale od Java 9 i JEP 280: Indify String Concatenation łączenie łańcuchów jest teraz wykonywane za pomocą
invokedynamic
i nieStringBuilder
. Ten artykuł pokazano różnice w kodzie bajtowym między Javą 8 a Javą 9.Jeśli test porównawczy uruchomiony ponownie w nowszej wersji Java nie pokazuje problemu, najprawdopodobniej nie ma błędu,
javac
ponieważ kompilator używa teraz nowego mechanizmu. Nie jestem pewien, czy zanurzenie się w zachowanie Java 8 jest korzystne, jeśli nastąpiła tak znacząca zmiana w nowszych wersjach.źródło
javac
.javac
generuje kod bajtowy i nie dokonuje żadnych wyrafinowanych optymalizacji. Uruchomiłem ten sam test porównawczy-XX:TieredStopAtLevel=1
i otrzymałem takie dane wyjściowe: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
więc gdy nie zoptymalizujemy wiele, obie metody dają takie same wyniki, problem ujawnia się dopiero po skompilowaniu kodu C2.invokedynamic
informuje tylko środowisko wykonawcze, aby zdecydowało, jak przeprowadzić konkatenację, a 5 z 6 strategii (w tym domyślnych) nadal używaStringBuilder
.StringConcatFactory.Strategy
enum?