Acelerando a criação do ConcurrentReferenceHashMap

Saudações, neste artigo, discutirei como você pode acelerar a criação do org.springframework.util.ConcurrentReferenceHashMap com pouco esforço.


Interessado em melhorar o desempenho? Bem-vindo


Inteligência


Vamos começar, é claro, com medições e tentar entender exatamente o que melhoraremos. Para isso, use o JMH 1.21, JDK 8 e JDK 11, bem como o async-profiler .


Para descobrir quanto é necessário para criar um dicionário vazio, colocamos uma experiência simples:


 @Benchmark public Object original() { return new ConcurrentReferenceHashMap(); } 

O perfil fica assim:


 55.21% 2429743 osuConcurrentReferenceHashMap.calculateShift 20.30% 891404 osuConcurrentReferenceHashMap$Segment.<init> 8.79% 387198 osuConcurrentReferenceHashMap.<init> 3.35% 147651 java.util.concurrent.locks.ReentrantLock.<init> 2.34% 102804 java.lang.ref.ReferenceQueue.<init> 1.61% 70748 osuConcurrentReferenceHashMap.createReferenceManager 1.53% 67265 osuConcurrentReferenceHashMap$Segment.createReferenceArray 0.78% 34493 java.lang.ref.ReferenceQueue$Lock.<init> 0.76% 33546 osuConcurrentReferenceHashMap$ReferenceManager.<init> 0.36% 15948 osuAssert.isTrue 

A direção é clara, você pode prosseguir.


Matemática


Portanto, gastamos a maior parte do tempo no método calculateShift . Aqui está:


 protected static int calculateShift(int minimumValue, int maximumValue) { int shift = 0; int value = 1; while (value < minimumValue && value < maximumValue) { value <<= 1; shift++; } return shift; } 

É difícil criar algo novo, então vamos começar a usá-lo:


 public ConcurrentReferenceHashMap(/*...*/ int concurrencyLevel, /*...*/) { //... this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); //... } // ConcurrentReferenceHashMap$Segment public Segment(int initialCapacity) { this.referenceManager = createReferenceManager(); this.initialSize = 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE); this.references = createReferenceArray(this.initialSize); this.resizeThreshold = (int) (this.references.length * getLoadFactor()); } 

Observe o uso do construtor Segment :


 int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); //... for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } 

O valor de roundedUpSegmentCapacity constante ao passar pelo loop; portanto, a expressão 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE) executada no construtor Segment também será sempre constante. Assim, podemos pegar a expressão especificada fora do construtor e do loop.


A mesma instrução é verdadeira para a expressão (int) (this.references.length * getLoadFactor()) , pois a matriz de references é criada usando a variável initialCapacity e seu tamanho é constante ao criar cada segmento. Puxe a expressão para fora dos limites do construtor e do loop.


Matrizes


Considere o método createReferenceArray :


 private Reference<K, V>[] createReferenceArray(int size) { return (Reference<K, V>[]) Array.newInstance(Reference.class, size); } 

Usando Array::newInstance claramente redundante, nada nos impede de criar uma matriz usando o construtor:


 private Reference<K, V>[] createReferenceArray(int size) { return new Reference[size]; } 

O desempenho do construtor não é inferior a chamar Array::newInstance no nível C2, mas superá-lo significativamente para pequenas matrizes nos modos C1 (propriedade -XX:TieredStopAtLevel=1 propriedade -XX:TieredStopAtLevel=1 ) e o interpretador (propriedade -Xint ):


 //C2 length Mode Cnt Score Error Units constructor 10 avgt 50 5,6 ± 0,0 ns/op constructor 100 avgt 50 29,7 ± 0,1 ns/op constructor 1000 avgt 50 242,7 ± 1,3 ns/op newInstance 10 avgt 50 5,5 ± 0,0 ns/op newInstance 100 avgt 50 29,7 ± 0,1 ns/op newInstance 1000 avgt 50 249,3 ± 9,6 ns/op //C1 length Mode Cnt Score Error Units constructor 10 avgt 50 6,8 ± 0,1 ns/op constructor 100 avgt 50 36,3 ± 0,6 ns/op constructor 1000 avgt 50 358,6 ± 6,4 ns/op newInstance 10 avgt 50 91,0 ± 2,4 ns/op newInstance 100 avgt 50 127,2 ± 1,8 ns/op newInstance 1000 avgt 50 322,8 ± 7,2 ns/op //-Xint length Mode Cnt Score Error Units constructor 10 avgt 50 126,3 ± 5,9 ns/op constructor 100 avgt 50 154,7 ± 2,6 ns/op constructor 1000 avgt 50 364,2 ± 6,2 ns/op newInstance 10 avgt 50 251,2 ± 11,3 ns/op newInstance 100 avgt 50 287,5 ± 11,4 ns/op newInstance 1000 avgt 50 486,5 ± 8,5 ns/op 

A substituição não afetará nossa referência, mas acelerará o código quando o aplicativo iniciar, quando o C2 ainda não funcionou. Mais sobre esse modo será dito no final do artigo.


Pequenas coisas importantes


Vamos voltar ao construtor ConcurrentReferenceHashMap


 ConcurrentReferenceHashMap(/*...*/) { Assert.isTrue(initialCapacity >= 0, "Initial capacity must not be negative"); Assert.isTrue(loadFactor > 0f, "Load factor must be positive"); Assert.isTrue(concurrencyLevel > 0, "Concurrency level must be positive"); Assert.notNull(referenceType, "Reference type must not be null"); this.loadFactor = loadFactor; this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); int size = 1 << this.shift; this.referenceType = referenceType; int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size); this.segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } } 

De um curioso para nós: substituir Array.newInstance por um construtor leva a um erro de compilação. Mas o ciclo é muito interessante, ou melhor, o apelo ao campo de segments . Para ver o quão devastador (às vezes) o desempenho pode ser, esse apelo pode ser recomendado pelo artigo de Nitzan Wakart, The Volátil Surprise Read .


Parece-me que o caso descrito no artigo se correlaciona com o código em questão. Foco nos segmentos:


 this.segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); } 

Imediatamente após a criação da matriz, ela é gravada no campo ConcurrentReferenceHashMap.segments e é com esse campo que o loop interage. Dentro do construtor Segment, há um registro nas references campo de volatilidade:


 private volatile Reference<K, V>[] references; public Segment(int initialCapacity) { //... this.references = createReferenceArray(this.initialSize); //... } 

Isso significa que é impossível melhorar o acesso ao campo de segments , ou seja, seu conteúdo é lido a cada volta do ciclo. Como verificar a verdade desta afirmação? A maneira mais fácil é copiar o código em um pacote separado e remover o volatile da declaração do campo Segment.references :


 protected final class Segment extends ReentrantLock { //  private volatile Reference<K, V>[] references; //  private Reference<K, V>[] references; } 

Verifique se algo mudou:


 @Benchmark public Object original() { return new tsypanov.map.original.ConcurrentReferenceHashMap(); } @Benchmark public Object nonVolatileSegmentReferences() { return new tsypanov.map.nonvolatile.ConcurrentReferenceHashMap(); } 

Encontramos ganhos significativos de desempenho (JDK 8):


 Benchmark Mode Cnt Score Error Units original avgt 100 732,1 ± 15,8 ns/op nonVolatileSegmentReferences avgt 100 610,6 ± 15,4 ns/op 

No JDK 11, o tempo gasto foi reduzido, mas a diferença relativa era quase inalterada:


 Benchmark Mode Cnt Score Error Units original avgt 100 473,8 ± 11,2 ns/op nonVolatileSegmentReferences avgt 100 401,9 ± 15,5 ns/op 

Evidentemente, o volatile precisa ser devolvido ao local e procurar outro caminho. Um gargalo é descoberto - este é um apelo ao campo. E se sim, então você pode criar a variável de segments , preencher a matriz e só então escrever no campo:


 Segment[] segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < segments.length; i++) { segments[i] = new Segment(roundedUpSegmentCapacity); } this.segments = segments; 

Como resultado, mesmo com melhorias simples, foi alcançado um bom crescimento:


Jdk 8


 Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 712,1 ± 7,2 ns/op patchedConcurrentReferenceHashMap avgt 100 496,5 ± 4,6 ns/op 

Jdk 11


 Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 536,0 ± 8,4 ns/op patchedConcurrentReferenceHashMap avgt 100 486,4 ± 9,3 ns/op 

O que a substituição de 'Arrays :: newInstance' por 'new T []' fornece


Ao iniciar o aplicativo Spring Booth da Idea, os desenvolvedores geralmente definem o sinalizador 'Ativar otimizações de inicialização', que adiciona -XX:TieredStopAtLevel=1 -noverify aos argumentos da VM, o que acelera o lançamento desativando a criação de perfil e o C2. Vamos fazer uma medição com os argumentos especificados:


 // JDK 8 -XX:TieredStopAtLevel=1 -noverify Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 1920,9 ± 24,2 ns/op patchedConcurrentReferenceHashMap avgt 100 592,0 ± 25,4 ns/op // JDK 11 -XX:TieredStopAtLevel=1 -noverify Benchmark Mode Cnt Score Error Units originalConcurrentReferenceHashMap avgt 100 1838,9 ± 8,0 ns/op patchedConcurrentReferenceHashMap avgt 100 549,7 ± 6,7 ns/op 

Aumento de mais de 3 vezes!


Para que é isso?


Em particular, isso é necessário para acelerar as consultas que retornam projeções no Spring Data JPA.



O perfil JMC mostra que a criação de um ConcurrentReferenceHashMap leva quase um quinto do tempo gasto na execução de uma consulta no formulário


 public interface SimpleEntityRepository extends JpaRepository<SimpleEntity, Long> { List<HasIdAndName> findAllByName(String name); } 

em que HasIdAndName é uma projeção de visualização


 public interface HasIdAndName { int getId(); String getName(); } 

Além disso, ConcurrentReferenceHashMap várias dezenas de vezes no código Spring, portanto, definitivamente não será supérfluo.


Conclusões


  • melhorar o desempenho não é tão difícil quanto parece à primeira vista
  • acesso volátil nas proximidades do ciclo é um dos possíveis gargalos
  • procure invariantes e tire-os dos ciclos

O que ler


Artigo de Nitzan Wakart


Exemplo de código


Alterações:
https://github.com/spring-projects/spring-framework/pull/1873
https://github.com/spring-projects/spring-framework/pull/2051

Source: https://habr.com/ru/post/pt432824/


All Articles