تحياتي ، في هذه المقالة سأناقش كيف يمكنك تسريع إنشاء org.springframework.util.ConcurrentReferenceHashMap
مع القليل من الجهد.
المهتمين في تعزيز الأداء؟ اهلا
الذكاء
سنبدأ ، بالطبع ، بالقياسات ونحاول أن نفهم ما الذي سنحسنه بالضبط. لهذا ، خذ JMH 1.21 و JDK 8 و JDK 11 ، وكذلك profiler async .
لمعرفة مقدار ما يلزم لإنشاء قاموس فارغ ، وضعنا تجربة بسيطة:
@Benchmark public Object original() { return new ConcurrentReferenceHashMap(); }
الملف الشخصي يشبه هذا:
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
الاتجاه واضح ، يمكنك المتابعة.
الرياضيات
لذلك ، نقضي حصة الأسد من الوقت في طريقة calculateShift
. ومن هنا:
protected static int calculateShift(int minimumValue, int maximumValue) { int shift = 0; int value = 1; while (value < minimumValue && value < maximumValue) { value <<= 1; shift++; } return shift; }
من الصعب التوصل إلى شيء جديد ، لذلك دعونا ننتقل إلى استخدامه:
public ConcurrentReferenceHashMap( int concurrencyLevel, ) {
لاحظ استخدام مُنشئ Segment
:
int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) / size);
تكون قيمة roundedUpSegmentCapacity
ثابتة عند المرور عبر الحلقة ، وبالتالي فإن التعبير 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE)
في مُنشئ Segment
سيكون ثابتًا دائمًا. وبالتالي ، يمكننا أن نتخذ التعبير المحدد خارج المنشئ والحلقة.
العبارة نفسها صحيحة للتعبير (int) (this.references.length * getLoadFactor())
، حيث يتم إنشاء مجموعة references
باستخدام المتغير initialCapacity
وحجمها ثابت عند إنشاء كل مقطع. سحب التعبير من حدود المنشئ وحلقة.
المصفوفات
createReferenceArray
طريقة createReferenceArray
:
private Reference<K, V>[] createReferenceArray(int size) { return (Reference<K, V>[]) Array.newInstance(Reference.class, size); }
استخدام Array::newInstance
بشكل واضح ، لا شيء يمنعنا من إنشاء صفيف باستخدام المُنشئ:
private Reference<K, V>[] createReferenceArray(int size) { return new Reference[size]; }
أداء المنشئ ليس أقل شأناً من استدعاء Array::newInstance
على مستوى C2 ، ولكن يتفوق عليه بشكل كبير للصفائف الصغيرة في أوضاع C1 (خاصية -XX:TieredStopAtLevel=1
) والمترجم (خاصية -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
لن يؤثر الاستبدال على المعيار الخاص بنا ، ولكنه سيسرع الكود عند بدء تشغيل التطبيق ، عندما لا يعمل C2 بعد. سيقال المزيد عن هذا الوضع في نهاية المقال.
الأشياء الصغيرة الرئيسية
دعنا ننتقل مرة أخرى إلى المنشئ 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); } }
من وجهة Array.newInstance
غريبة بالنسبة لنا: استبدال Array.newInstance
يؤدي إلى حدوث خطأ في Array.newInstance
البرمجي ، نمر. لكن الدورة مثيرة جدًا للاهتمام ، أو بالأحرى نداء إلى مجال segments
. لنرى كيف يمكن أن يكون الأداء المدمر (في بعض الأحيان) ، يمكن تقديم المشورة لمثل هذا النداء من خلال مقال Nitzan Wakart " مفاجأة القراءة المتقلبة" .
يبدو أن الحالة الموصوفة في المقال مرتبطة بالكود المعني. التركيز على القطاعات:
this.segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < this.segments.length; i++) { this.segments[i] = new Segment(roundedUpSegmentCapacity); }
مباشرة بعد إنشاء الصفيف ، تتم كتابته إلى الحقل ConcurrentReferenceHashMap.segments
، ويتفاعل مع هذا الحقل مع هذا الحقل. داخل مُنشئ Segment ، يوجد سجل في references
حقل التقلب:
private volatile Reference<K, V>[] references; public Segment(int initialCapacity) {
هذا يعني أنه من المستحيل تحسين الوصول إلى حقل segments
، بمعنى آخر ، تتم قراءة محتوياته في كل منعطف من الدورة. كيفية التحقق من صحة هذا البيان؟ أسهل طريقة هي نسخ الكود في حزمة منفصلة وإزالة volatile
من إعلان الحقل Segment.references
:
protected final class Segment extends ReentrantLock {
تحقق مما إذا كان هناك شيء قد تغير:
@Benchmark public Object original() { return new tsypanov.map.original.ConcurrentReferenceHashMap(); } @Benchmark public Object nonVolatileSegmentReferences() { return new tsypanov.map.nonvolatile.ConcurrentReferenceHashMap(); }
نجد مكاسب أداء كبيرة (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
في JDK 11 ، تم تقليل الوقت المستغرق ، ولكن الفجوة النسبية لم تتغير تقريبًا:
Benchmark Mode Cnt Score Error Units original avgt 100 473,8 ± 11,2 ns/op nonVolatileSegmentReferences avgt 100 401,9 ± 15,5 ns/op
بالطبع ، يجب إعادة volatile
إلى المكان والبحث عن طريقة أخرى. تم اكتشاف عنق الزجاجة - وهذا هو نداء إلى الميدان. وإذا كان الأمر كذلك ، فيمكنك إنشاء متغير segments
، وملء المصفوفة وبعد ذلك فقط اكتبها في الحقل:
Segment[] segments = (Segment[]) Array.newInstance(Segment.class, size); for (int i = 0; i < segments.length; i++) { segments[i] = new Segment(roundedUpSegmentCapacity); } this.segments = segments;
نتيجة لذلك ، حتى مع هذه التحسينات البسيطة ، تحقق نمو جيد:
جدك 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
جدك 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
ماذا يعني استبدال "Arrays :: newInstance" بعبارة "T [] جديدة"
عند بدء تشغيل تطبيق Spring Booth من Idea ، غالبًا ما يقوم المطورون بتعيين العلامة "تمكين تحسينات التشغيل" ، التي تضيف -XX:TieredStopAtLevel=1 -noverify
إلى وسيطات VM ، مما يسرع عملية الإطلاق عن طريق تعطيل التوصيف و C2. لنقم بإجراء القياس باستخدام الوسائط المحددة:
// 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
أكثر من 3 أضعاف الزيادة!
ما هذا؟
على وجه الخصوص ، هذا ضروري لتسريع الاستعلامات التي تعيد التوقعات في Spring Data JPA.

يُظهر ملف تعريف JMC أن إنشاء ConcurrentReferenceHashMap
يستغرق حوالي خمس الوقت الذي يستغرقه تنفيذ استعلام النموذج
public interface SimpleEntityRepository extends JpaRepository<SimpleEntity, Long> { List<HasIdAndName> findAllByName(String name); }
حيث HasIdAndName
هو عرض الإسقاط
public interface HasIdAndName { int getId(); String getName(); }
أيضا ، ConcurrentReferenceHashMap
عدة عشرات المرات في رمز الربيع ، لذلك بالتأكيد لن يكون لزوم لها.
الاستنتاجات
- تحسين الأداء ليس بالأمر الصعب كما يبدو للوهلة الأولى
- الوصول المتقلب بالقرب من الدورة هو أحد الاختناقات المحتملة
- ابحث عن المتغيرات وأخذها خارج الدورات
ماذا تقرأ
مقال بقلم نيتزان فكرت
مثال رمز
التغييرات:
https://github.com/spring-projects/spring-framework/pull/1873
https://github.com/spring-projects/spring-framework/pull/2051