समवर्तीReferenceHashMap के निर्माण में तेजी

अभिवादन, इस नोट में मैं चर्चा करूँगा कि कैसे आप थोड़े प्रयास से org.springframework.util.ConcurrentReferenceHashMap के निर्माण को गति दे सकते हैं।


प्रदर्शन को बढ़ाने में दिलचस्पी है? आपका स्वागत है!


अन्वेषण


हम निश्चित रूप से माप के साथ शुरू करेंगे और यह समझने की कोशिश करेंगे कि वास्तव में हम क्या सुधार करेंगे। इसके लिए, JMH 1.21, JDK 8 और JDK 11, साथ ही साथ async-profiler लें


खाली शब्दकोष बनाने में कितना लगता है, यह जानने के लिए, हमने एक सरल अनुभव दिया:


 @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, /*...*/) { //... 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()); } 

Segment कंस्ट्रक्टर के उपयोग पर ध्यान दें:


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

लूप से गुजरने पर roundedUpSegmentCapacity मान स्थिर होता है, इसलिए Segment कंस्ट्रक्टर में निष्पादित किया गया एक्सप्रेशन 1 << calculateShift(initialCapacity, MAXIMUM_SEGMENT_SIZE) भी हमेशा स्थिर रहेगा। इस प्रकार, हम कंस्ट्रक्टर और लूप के बाहर निर्दिष्ट अभिव्यक्ति ले सकते हैं।


एक ही कथन अभिव्यक्ति (int) (this.references.length * getLoadFactor()) , क्योंकि initialCapacity वैरिएबल का उपयोग करके references सरणी बनाई गई है और प्रत्येक खंड बनाते समय इसका आकार स्थिर है। कंस्ट्रक्टर और लूप की सीमा से बाहर अभिव्यक्ति खींचो।


सरणियों


createReferenceArray विधि पर विचार करें:


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

Array::newInstance का उपयोग करना Array::newInstance स्पष्ट रूप से निरर्थक है, कुछ भी हमें कंस्ट्रक्टर का उपयोग करके एक सरणी बनाने से नहीं रोकता है:


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

कंस्ट्रक्टर का प्रदर्शन C2 स्तर पर Array::newInstance को कॉल करने से हीन नहीं है, लेकिन C1 मोड्स ( -XX:TieredStopAtLevel=1 प्रॉपर्टी -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 जगह एक संकलन त्रुटि की ओर जाता है, जिससे हम गुजरते हैं। लेकिन चक्र बहुत दिलचस्प है, या बल्कि, segments फ़ील्ड के लिए अपील करता segments । यह देखने के लिए कि कितना विनाशकारी (कभी-कभी) प्रदर्शन हो सकता है, इस तरह की अपील को निट्ज़ैन वकार्ट के लेख द वाष्पशील पढ़ा सुपरराइज द्वारा सलाह दी जा सकती है।


लेख में वर्णित मामला, यह मुझे लगता है, प्रश्न में कोड के साथ सहसंबंधित है। खंडों पर ध्यान दें:


 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 फ़ील्ड में लिखा गया है, और यह इस फ़ील्ड के साथ है कि लूप इंटरैक्ट करता है। खंड निर्माणकर्ता के अंदर, अस्थिरता क्षेत्र references में एक रिकॉर्ड है:


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

इसका मतलब यह है कि segments फ़ील्ड तक पहुंच में सुधार करना असंभव है, दूसरे शब्दों में, इसकी सामग्री को चक्र के प्रत्येक मोड़ पर पढ़ा जाता है। इस कथन की सच्चाई को कैसे सत्यापित करें? सबसे आसान तरीका है कि कोड को एक अलग पैकेज में कॉपी करें और Segment.references की घोषणा के क्षेत्र से volatile को हटा दें:


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

जांचें कि क्या कुछ बदल गया है:


 @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; 

परिणामस्वरूप, इस तरह के सरल सुधारों के साथ, एक अच्छी वृद्धि हासिल की गई:


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 

'Arrays :: newInstance' की जगह 'new T []' 'क्या देता है


आइडिया से स्प्रिंग बूथ एप्लिकेशन लॉन्च करते समय, डेवलपर्स अक्सर फ्लैग 'इनेबल लॉन्च ऑप्टिमाइजेशन' सेट करते हैं, जो जोड़ता है -XX:TieredStopAtLevel=1 -noverify VM तर्कों के लिए, जो कि रूपरेखा और C2 को -XX:TieredStopAtLevel=1 -noverify लॉन्च की गति -XX:TieredStopAtLevel=1 -noverify है। आइए निर्दिष्ट तर्कों के साथ माप करें:


 // 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 गुना से अधिक की वृद्धि!


यह किस लिए है?


विशेष रूप से, स्प्रिंग डेटा JPA में अनुमानों को लौटाने वाले प्रश्नों को गति देने के लिए यह आवश्यक है।



JMC प्रोफ़ाइल से पता चलता है कि एक ConcurrentReferenceHashMap बनाने से फ़ॉर्म की एक क्वेरी निष्पादित करने में लगभग पांचवां समय लगता है


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

जहाँ HasIdAndName एक दृश्य प्रक्षेपण है


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

इसके अलावा, ConcurrentReferenceHashMap स्प्रिंग कोड में कई दसियों बार ConcurrentReferenceHashMap , इसलिए यह निश्चित रूप से शानदार नहीं होगा।


निष्कर्ष


  • प्रदर्शन में सुधार करना उतना मुश्किल नहीं है जितना पहली नज़र में लगता है
  • चक्र के आसपास के क्षेत्र में अस्थिर पहुंच संभव बाधाओं में से एक है
  • आक्रमणकारियों की तलाश करें और उन्हें साइकिल से बाहर निकालें

क्या पढ़ना है?


निट्ज़ैन वकार्ट द्वारा अनुच्छेद


कोड उदाहरण


परिवर्तन:
https://github.com/spring-projects/spring-framework/pull/1873
https://github.com/spring-projects/spring-framework/pull/2051

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


All Articles