Mempercepat pembuatan ConcurrentReferenceHashMap

Salam, dalam artikel ini saya akan membahas bagaimana Anda dapat mempercepat pembuatan org.springframework.util.ConcurrentReferenceHashMap dengan sedikit usaha.


Tertarik untuk meningkatkan kinerja? Selamat datang


Kecerdasan


Kami akan mulai, tentu saja, dengan pengukuran dan mencoba memahami apa yang sebenarnya akan kami tingkatkan. Untuk ini, ambil JMH 1.21, JDK 8 dan JDK 11, serta async-profiler .


Untuk mengetahui berapa banyak yang diperlukan untuk membuat kamus kosong, kami berikan pengalaman sederhana:


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

Profilnya terlihat seperti ini:


 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 

Arahnya jelas, Anda bisa melanjutkan.


Matematika


Jadi, kami menghabiskan sebagian besar waktu dalam metode calculShift. Ini dia:


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

Sulit membuat sesuatu yang baru, jadi mari kita beralih untuk menggunakannya:


 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()); } 

Perhatikan penggunaan konstruktor Segment :


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

Nilai roundedUpSegmentCapacity konstan ketika melewati loop, oleh karena itu ungkapan 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE) dieksekusi dalam konstruktor Segment juga akan selalu konstan. Dengan demikian, kita dapat mengambil ekspresi yang ditentukan di luar konstruktor dan loop.


Pernyataan yang sama berlaku untuk ekspresi (int) (this.references.length * getLoadFactor()) , karena array references dibuat menggunakan variabel initialCapacity dan ukurannya konstan ketika membuat setiap segmen. Tarik ekspresi keluar dari batas konstruktor dan loop.


Array


Pertimbangkan metode createReferenceArray :


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

Menggunakan Array::newInstance jelas berlebihan, tidak ada yang mencegah kita membuat array menggunakan konstruktor:


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

Kinerja konstruktor tidak kalah dengan memanggil Array::newInstance pada level C2, tetapi secara signifikan melampaui untuk array kecil dalam mode C1 (properti -XX:TieredStopAtLevel=1 ) dan interpreter ( -Xint property):


 //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 

Penggantian tidak akan memengaruhi tolok ukur kami, tetapi akan mempercepat kode saat aplikasi dimulai, saat C2 belum berfungsi. Lebih lanjut tentang mode ini akan dikatakan di akhir artikel.


Hal-hal kecil yang penting


Mari kita kembali ke konstruktor 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); } } 

Dari yang penasaran bagi kami: mengganti Array.newInstance dengan konstruktor mengarah ke kesalahan kompilasi, kami melewatinya. Namun siklus ini sangat menarik, atau lebih tepatnya, daya tarik ke bidang segments . Untuk melihat seberapa dahsyatnya kinerja (kadang-kadang), banding seperti itu dapat disarankan oleh artikel Nitzan Wakart.


Kasus yang dijelaskan dalam artikel, menurut saya, berkorelasi dengan kode yang dimaksud. Fokus pada segmen:


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

Segera setelah membuat array, ini ditulis ke bidang ConcurrentReferenceHashMap.segments , dan dengan bidang inilah loop berinteraksi. Di dalam konstruktor Segmen, ada catatan di references bidang volatilitas:


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

Ini berarti bahwa mustahil untuk meningkatkan akses ke bidang segments , dengan kata lain, isinya dibaca pada setiap putaran siklus. Bagaimana cara memverifikasi kebenaran pernyataan ini? Cara termudah adalah menyalin kode ke paket terpisah dan menghapus volatile dari deklarasi bidang Segment.references :


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

Periksa apakah ada sesuatu yang berubah:


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

Kami menemukan keuntungan kinerja yang signifikan (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 

Pada JDK 11, waktu yang dihabiskan berkurang, tetapi kesenjangan relatif hampir tidak berubah:


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

Tentu saja, volatile perlu dikembalikan ke tempat itu dan mencari cara lain. Kemacetan ditemukan - ini adalah daya tarik ke lapangan. Dan jika demikian, maka Anda dapat membuat variabel segments , mengisi array dan hanya kemudian menuliskannya di bidang:


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

Akibatnya, bahkan dengan perbaikan sederhana seperti itu, pertumbuhan yang baik tercapai:


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 

Apa yang menggantikan 'Array :: newInstance' dengan 'new T []' beri


Saat meluncurkan aplikasi Spring Booth dari Idea, pengembang sering menetapkan bendera 'Aktifkan optimisasi peluncuran', yang menambahkan -XX:TieredStopAtLevel=1 -noverify argumen VM, yang mempercepat peluncuran dengan menonaktifkan profil dan C2. Mari kita melakukan pengukuran dengan argumen yang ditentukan:


 // 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 

Lebih dari 3 kali lipat peningkatan!


Untuk apa ini?


Secara khusus, ini diperlukan untuk mempercepat permintaan proyeksi kembali di Spring Data JPA.



Profil JMC menunjukkan bahwa membuat ConcurrentReferenceHashMap membutuhkan hampir seperlima dari waktu yang dihabiskan untuk mengeksekusi kueri form


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

di mana HasIdAndName adalah proyeksi tampilan


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

Juga, ConcurrentReferenceHashMap beberapa puluh kali dalam kode Spring, jadi itu pasti tidak akan berlebihan.


Kesimpulan


  • meningkatkan kinerja tidaklah sesulit kelihatannya pada pandangan pertama
  • akses volatile di sekitar siklus adalah salah satu hambatan yang mungkin terjadi
  • mencari invarian dan mengeluarkannya dari siklus

Apa yang harus dibaca


Artikel oleh Nitzan Wakart


Contoh kode


Perubahan:
https://github.com/spring-projects/spring-framework/pull/1873
https://github.com/spring-projects/spring-framework/pull/2051

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


All Articles