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, ) {
Perhatikan penggunaan konstruktor Segment
:
int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size);
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) {
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 {
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