Recentemente, encontrei um problema relacionado à concatenação de String. Este benchmark resume:
@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();
}
}
}
No JDK 1.8.0_222 (VM do servidor OpenJDK de 64 bits, 25.222-b10), obtive os seguintes resultados:
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
Parece um problema semelhante ao JDK-8043677 , em que uma expressão com efeito colateral interrompe a otimização da nova StringBuilder.append().append().toString()
cadeia. Mas o código por Class.getName()
si só não parece ter efeitos colaterais:
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();
A única coisa suspeita aqui é uma chamada ao método nativo que acontece de fato apenas uma vez e seu resultado é armazenado em cache no campo da classe. No meu benchmark, eu o coloquei em cache explicitamente no método de instalação.
Eu esperava que o preditor de ramificação descobrisse que, a cada chamada de referência, o valor real de this.name nunca é nulo e otimiza toda a expressão.
No entanto, enquanto BrokenConcatenationBenchmark.fast()
eu tenho o seguinte:
@ 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)
ou seja, o compilador é capaz de alinhar tudo, BrokenConcatenationBenchmark.slow()
pois é diferente:
@ 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)
Portanto, a questão é se esse é o comportamento apropriado da JVM ou do bug do compilador?
Estou fazendo a pergunta, porque alguns dos projetos ainda estão usando o Java 8 e, se não for corrigido em nenhuma atualização de versão, é razoável içar chamadas Class.getName()
manualmente a partir de pontos de acesso.
PS Nos JDKs mais recentes (11, 13, 14-eap), o problema não é reproduzido.
fonte
this.name
.Class.getName()
e nosetUp()
método, não no corpo da referência.Respostas:
A JVM do HotSpot coleta estatísticas de execução por bytecode. Se o mesmo código for executado em contextos diferentes, o perfil do resultado agregará estatísticas de todos os contextos. Esse efeito é conhecido como poluição do perfil .
Class.getName()
é obviamente chamado não apenas a partir do seu código de referência. Antes de o JIT começar a compilar o benchmark, ele já sabe que a seguinte condiçãoClass.getName()
foi atendida várias vezes:Pelo menos, tempos suficientes para tratar este ramo estatisticamente importante. Portanto, o JIT não excluiu esse ramo da compilação e, portanto, não pôde otimizar a concatração de cadeias devido a um possível efeito colateral.
Isso nem precisa ser uma chamada de método nativa. Apenas uma atribuição de campo regular também é considerada um efeito colateral.
Aqui está um exemplo de como a poluição do perfil pode prejudicar outras otimizações.
Esta é basicamente a versão modificada do seu benchmark que simula a poluição do
getName()
perfil. Dependendo do número degetName()
chamadas preliminares em um objeto novo, o desempenho adicional da concatenação de cadeias de caracteres pode diferir drasticamente:Mais exemplos de poluição de perfil »
Não posso chamá-lo de bug ou de "comportamento apropriado". É assim que a compilação adaptativa dinâmica é implementada no HotSpot.
fonte
Ligeiramente não relacionado, mas desde o Java 9 e o JEP 280: Indicar concatenação de cadeias, a concatenação de cadeias agora é feita com
invokedynamic
e nãoStringBuilder
. Este artigo mostra as diferenças no bytecode entre Java 8 e Java 9.Se o benchmark executado novamente na versão mais recente do Java não mostrar o problema, provavelmente não haverá nenhum bug
javac
porque o compilador agora usa um novo mecanismo. Não tenho certeza se mergulhar no comportamento do Java 8 é benéfico se houver uma mudança substancial nas versões mais recentes.fonte
javac
.javac
gera bytecode e não faz nenhuma otimização sofisticada. Eu executei o mesmo benchmark-XX:TieredStopAtLevel=1
e recebi esta saída: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
Portanto, quando não otimizamos muito, os dois métodos produzem os mesmos resultados, o problema se revela apenas quando o código é compilado em C2.invokedynamic
diz apenas ao tempo de execução para escolher como fazer a concatenação e 5 de 6 estratégias (incluindo o padrão) ainda usamStringBuilder
.StringConcatFactory.Strategy
enum?