बर्बाद प्रदर्शन

यह नोट JPoint 2018 सम्मेलन से मेरी रिपोर्ट "अक्षम कोड का उपयोग करके प्रदर्शन को कैसे बर्बाद करें" का एक लिखित संस्करण है। आप सम्मेलन पृष्ठ पर वीडियो और स्लाइड देख सकते हैं। अनुसूची में, रिपोर्ट को स्मूथी के एक आक्रामक ग्लास के साथ चिह्नित किया जाता है, इसलिए सुपर जटिल कुछ भी नहीं होगा, शुरुआती लोगों के लिए यह अधिक संभावना है।


रिपोर्ट का विषय:


  • इसमें अड़चनों को खोजने के लिए कोड को कैसे देखें
  • आम एंटीपार्टर्न
  • गैर-स्पष्ट रेक
  • रेक बायपास

किनारे पर, उन्होंने रिपोर्ट में कुछ गलतियाँ / चूक बताई हैं, उन्हें यहाँ नोट किया गया है। टिप्पणियाँ भी स्वागत है।


प्रदर्शन पर प्रभाव


एक उपयोगकर्ता वर्ग है:


class User { String name; int age; } 

हमें वस्तुओं की एक दूसरे से तुलना करने की आवश्यकता है, इसलिए हम equals और hashCode विधियों की घोषणा करते हैं:


 import lombok.EqualsAndHashCode; @EqualsAndHashCode class User { String name; int age; } 

कोड व्यावहारिक है, सवाल अलग है: क्या इस कोड का प्रदर्शन सबसे अच्छा होगा? इसका उत्तर देने के लिए, आइए Object::equals की विशेषताओं को याद करते हैं Object::equals विधि: यह केवल तभी सकारात्मक परिणाम देता है जब तुलना किए जा रहे सभी फ़ील्ड समान हों, अन्यथा परिणाम नकारात्मक होगा। दूसरे शब्दों में, एक अंतर पहले से ही एक नकारात्मक परिणाम के लिए पर्याप्त है।


@EqualsAndHashCode लिए जनरेट किए गए कोड को देखने के @EqualsAndHashCode हम कुछ इस तरह देखेंगे:


 public boolean equals(Object that) { //... if (name == null && that.name != null) { return false; } if (name != null && !name.equals(that.name)) { return false; } return age == that.age; } 

खेतों की जाँच करने का क्रम उनकी घोषणा के क्रम से मेल खाता है, जो हमारे मामले में सबसे अच्छा समाधान नहीं है, क्योंकि equals उपयोग करने वाली वस्तुओं की तुलना करना सरल प्रकारों की तुलना में "कठिन" है।


ठीक है, आइए आइडिया का उपयोग करके equals/hashCode विधियों को बनाने की कोशिश करते हैं:


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(name, that.name); } 

एक आइडिया स्मार्ट कोड बनाता है जो विभिन्न प्रकार के डेटा की तुलना करने की जटिलता को जानता है। खैर, हम @EqualsAndHashCode को @EqualsAndHashCode और हम स्पष्ट रूप से equals/hashCode @EqualsAndHashCode लिखेंगे। अब देखते हैं कि जब क्लास बढ़ती है तो क्या होता है:


 class User { List<T> props; String name; int age; } 

equals/hashCode :


 @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } User that = (User) o; return age == that.age && Objects.equals(props, that.props) // <---- && Objects.equals(name, that.name); } 

स्ट्रिंग्स की तुलना करने से पहले सूची की तुलना की जाती है, जो स्ट्रिंग्स अलग होने पर कोई मतलब नहीं है। पहली नज़र में, बहुत अंतर नहीं है, क्योंकि समान लंबाई के तारों की तुलना संकेतों द्वारा की जाती है (यानी, स्ट्रिंग की लंबाई के साथ तुलना समय बढ़ता है):


एक अशुद्धि थी

java.lang.String::equals विधि घुसपैठ है , इसलिए निष्पादन पर तुलना पर कोई संकेत नहीं है।


 //java.lang.String public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { // <---- if (v1[i] != v2[i]) return false; i++; } return true; } } return false; } 

अब दो ArrayList (सबसे अधिक इस्तेमाल की जाने वाली सूची कार्यान्वयन के रूप में) की तुलना करने पर विचार करें। ArrayList , हम यह जानकर आश्चर्यचकित हैं कि इसमें equals का अपना कार्यान्वयन नहीं है, लेकिन एक अंतर्निहित कार्यान्वयन का उपयोग करता है:



 //AbstractList::equals public boolean equals(Object o) { if (o == this) { return true; } if (!(o instanceof List)) { return false; } ListIterator<E> e1 = listIterator(); ListIterator<?> e2 = ((List<?>) o).listIterator(); while (e1.hasNext() && e2.hasNext()) { // <---- E o1 = e1.next(); Object o2 = e2.next(); if (!(o1 == null ? o2 == null : o1.equals(o2))) { return false; } } return !(e1.hasNext() || e2.hasNext()); } 

यहाँ महत्वपूर्ण है दो पुनरावृत्तियों का निर्माण और उनके माध्यम से जोड़ीदार मार्ग। मान लें कि दो ArrayList :


  • 1 से 99 तक एक संख्या में
  • 1 से 100 तक दूसरी संख्या में

आदर्श रूप से, यह दो सूचियों के आकार की तुलना करने के लिए पर्याप्त होगा और यदि वे मेल नहीं खाते हैं, तो तुरंत एक नकारात्मक परिणाम लौटाते हैं (जैसा कि AbstractSet करता है), वास्तव में, 99 तुलनाएं की जाएंगी और केवल सौवें पर यह स्पष्ट हो जाएगा कि सूचियां अलग हैं।


कोटलीनियों के साथ क्या है?


 data class User(val name: String, val age: Int); 

यहाँ सब कुछ लोम्बोक जैसा है - तुलना क्रम घोषणा के क्रम से मेल खाता है:


 public boolean equals(Object o) { if (this == o) { return true; } if (o instanceof User) { User u = (User) o; if (Intrinsics.areEqual(name, u.name) && age == u.age) { // <---- return true; } } return false; } 

कैसे सुधरे हालात? - शिकायत करो!

IDEA-170178 संग्रह की तुलना IDEA- जनित बराबरी के अंत में की जानी चाहिए ()
https://youtrack.jetbrains.com/issue/IDEA-170178


बेहतर प्रदर्शन # 1543 के लिए @EqualsAndHashCode में तुलना का पुन: परीक्षण
https://github.com/rzwitserloot/lombok/issues/1543


डेटा वर्ग का KT-23184 ऑटो-जेनरेट किया गया समान () इष्टतम नहीं है
https://youtrack.jetbrains.com/issue/KT-23184


वर्कअराउंड के रूप में, आप मैन्युअल रूप से फ़ील्ड घोषणाओं को व्यवस्थित कर सकते हैं।


आइए कार्य को जटिल करते हैं


 void check(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); boolean valid = dto.isValid(); if (valid && entity.hasGoodRating()) { // <---- //do smth } } 

कोड में डेटाबेस तक पहुंच शामिल है, यहां तक ​​कि जब तीर द्वारा इंगित शर्तों की जांच करने का परिणाम अग्रिम में अनुमानित है। यदि valid चर का मान गलत है, तो if ब्लॉक में कोड कभी निष्पादित नहीं होगा, जिसका अर्थ है कि आप अनुरोध के बिना कर सकते हैं:


 void check(Dto dto) { boolean valid = dto.isValid(); if (valid && hasGoodRating(dto)) { //do smth } } //       ,    boolean hasGoodRating(Dto dto) { SomeEntity entity = jpaRepository.findOne(dto.getId()); return entity.hasGoodRating(); } 

किनारे से ध्यान दें

जब इकाई JpaRepository::findOne से वापस लौटाया जा सकता है तो निरर्थक हो सकता है JpaRepository::findOne पहले JpaRepository::findOne ही पहले स्तर के कैश में है - फिर कोई अनुरोध नहीं होगा।


स्पष्ट शाखा के बिना एक समान उदाहरण:


 boolean checkChild(Dto dto) { Long id = dto.getId(); Entity entity = jpaRepository.findOne(id); return dto.isValid() && entity.hasChild(); } 

एक त्वरित वापसी आपको अनुरोध में देरी करने की अनुमति देता है:


 boolean checkChild(Dto dto) { if (!dto.isValid()) { return false; } return jpaRepository.findOne(dto.getId()).hasChild(); } 

एक स्पष्ट रूप से स्पष्ट जोड़ जो रिपोर्ट में दिखाई नहीं दिया

कल्पना करें कि एक निश्चित चेक एक समान इकाई का उपयोग करता है:


 @Entity class ParentEntity { @ManyToOne(fetch = LAZY) @JoinColumn(name = "CHILD_ID") private ChildEntity child; @Enumerated(EnumType.String) private SomeType type; 

यदि चेक एक ही इकाई का उपयोग करता है, तो आपको यह सुनिश्चित करना चाहिए कि पहले से लोड किए गए फ़ील्ड में कॉल के बाद "आलसी" बाल संस्थाओं / संग्रह को कॉल किया जाता है। पहली नज़र में, एक अतिरिक्त अनुरोध का समग्र चित्र पर महत्वपूर्ण प्रभाव नहीं पड़ेगा, लेकिन जब कोई कार्य लूप में किया जाता है तो सब कुछ बदल सकता है।


निष्कर्ष: व्यक्तिगत कार्यों की बढ़ती जटिलता के क्रम में कार्यों / चेकों की श्रृंखला का आदेश दिया जाना चाहिए, शायद उनमें से कुछ को प्रदर्शन नहीं करना होगा।


चक्र और थोक प्रसंस्करण


निम्नलिखित उदाहरण को विशेष स्पष्टीकरण की आवश्यकता नहीं है:


 @Transactional void enrollStudents(Set<Long> ids) { for (Long id : ids) { Student student = jpaRepository.findOne(id); // <---- O(n) enroll(student); } } 

कई डेटाबेस प्रश्नों के कारण, कोड धीमा है।


टिप्पणी

प्रदर्शन तब और भी अधिक डूब सकता है, जब किसी लेनदेन के बाहर enrollStudents विधि निष्पादित की जाती है: तो osdjrJpaRepository::findOne प्रत्येक कॉल osdjrJpaRepository::findOne को एक नए लेन-देन ( SimpleJpaRepository देखें) में निष्पादित किया जाएगा, जिसका अर्थ है कि डेटाबेस से कनेक्शन प्राप्त करना और वापस करना, साथ ही साथ प्रथम स्तर कैश बनाना और फ्लशिंग करना।


ठीक:


 @Transactional void enrollStudents(Set<Long> ids) { if (ids.isEmpty()) { return; } for (Student student : jpaRepository.findAll(ids)) { enroll(student); } } 

चलो चाबियों के संग्रह के लिए रनटाइम (माइक्रोसेकंड में) को मापें (10 और 100 टुकड़े)


बेंचमार्क


टिप्पणी

यदि आप Oracle का उपयोग करते हैं और findAll लिए 1000 से अधिक कुंजियाँ पास findAll , तो आपको अपवाद ORA-01795: maximum number of expressions in a list is 1000 findAll मिलेगा ORA-01795: maximum number of expressions in a list is 1000
इसके अलावा, एक भारी (कई कुंजियों के साथ) -सीरीज in प्रदर्शन करना एन प्रश्नों से भी बदतर हो सकता है। यह सब विशिष्ट अनुप्रयोग पर निर्भर करता है, इसलिए चक्र के बड़े पैमाने पर प्रसंस्करण के लिए यांत्रिक प्रतिस्थापन प्रदर्शन को नीचा कर सकता है।


एक ही विषय पर एक अधिक जटिल उदाहरण


 for (Long id : ids) { Region region = jpaRepository.findOne(id); if (region == null) { // <----  region = new Region(); region.setId(id); } use(region); } 

इस स्थिति में, हम लूप को JpaRepository::findAll प्रतिस्थापित नहीं कर सकते, JpaRepository::findAll यह तर्क को तोड़ देगा: JpaRepository::findAll से प्राप्त सभी मान JpaRepository::findAll null नहीं होगा और if ब्लॉक काम नहीं करेगा।


तथ्य यह है कि प्रत्येक डेटाबेस कुंजी के लिए हमें इस कठिनाई को हल करने में मदद करेगा
वास्तविक मूल्य या उसकी अनुपस्थिति या तो देता है। यानी एक अर्थ में, एक डेटाबेस एक शब्दकोश है। बॉक्स से जावा हमें डिक्शनरी का तैयार किया हुआ कार्यान्वयन देता है - HashMap - जिसके शीर्ष पर हम डेटाबेस को बदलने के लिए तर्क का निर्माण करेंगे:


 Map<Long, Region> regionMap = jpaRepository.findAll(ids) .stream() .collect(Collectors.toMap(Region::getId, Function.identity())); for (Long id : ids) { Region region = map.get(id); if (region == null) { region = new Region(); region.setId(id); } use(region); } 

उल्टा उदाहरण


 // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

यह कोड हमेशा संस्थाओं की सूची को बचाने के लिए एक नया लेनदेन बनाता है। एक नए लेनदेन को खोलने वाले तरीके से कई कॉल करने के साथ शुरू होता है:


 //   @Transactional public void audit(List<AuditDto> inserts) { inserts.map(this::toEntities).forEach(saver::save); // <---- } // class Saver @Transactional(propagation = Propagation.REQUIRES_NEW) // <---- public void save(List<AuditEntity> entities) { jpaRepository.save(entities); } 

समाधान: Saver::save लागू करें Saver::save संपूर्ण डेटा सेट के लिए तुरंत विधि Saver::save :


 @Transactional public void audit(List<AuditDto> inserts) { List<AuditEntity> bulk = inserts .map(this::toEntities) .flatMap(List::stream) // <---- .collect(toList()); saver.save(bulk); } 

बहुत सारे लेन-देन एक में विलय हो जाते हैं, जो एक ठोस वृद्धि (माइक्रोसेकंड में समय) देता है:


बेंचमार्क


कई लेन-देन के साथ एक उदाहरण को औपचारिक बनाना मुश्किल है, जिसे JpaRepository::findOne कॉल करने के बारे में नहीं कहा जा सकता है JpaRepository::findOne एक लूप में।


इसलिए, हम उपाय करेंगे

https://youtrack.jetbrains.com/issue/IDEA-165730
IDEA-165730 JpaRepository के अक्षम उपयोग के बारे में चेतावनी


https://youtrack.jetbrains.com/issue/IDEA-165942
आईडिया -165 942 बल्क ऑपरेशन के साथ लूप में विधि कॉल को बदलने के लिए निरीक्षण
2017.1 में तय किया गया


दृष्टिकोण न केवल डेटाबेस पर लागू होता है, इसलिए टैगिर लानी वलेव आगे बढ़ गए। और अगर पहले हमने ऐसा लिखा था:


 List<Long> list = new ArrayList<>(); for (Long id : items) { list.add(id); } 

और सब कुछ ठीक था, अब "आइडिया" खुद को सही करने का सुझाव देता है:


 List<Long> list = new ArrayList<>(); list.addAll(items); 

लेकिन यहां तक ​​कि यह विकल्प हमेशा इसे संतुष्ट नहीं करता है, क्योंकि आप इसे और भी छोटा और तेज बना सकते हैं:


 List<Long> list = new ArrayList<>(items); 

तुलना (एनएस में समय)

ArrayList के लिए, यह सुधार ध्यान देने योग्य वृद्धि देता है:



हैशसेट के लिए, यह इतना रसदार नहीं है:



बेंचमार्क


डेवलपर के लिए उपयोगी:

https://youtrack.jetbrains.com/issue/IDEA-138456
आईडिया-138,456 नया निरीक्षण: Collection.addAll () पैराट्राइज्ड कंस्ट्रक्टर के साथ बदली जाने योग्य
142.1217 में तय किया गया


https://youtrack.jetbrains.com/issue/IDEA-178761
आईडिया-178 761 निरीक्षण 'Collection.addAll () पैरामीरिज्ड कंस्ट्रक्टर के साथ बदली जाने वाली' डिफ़ॉल्ट रूप से चालू होना चाहिए
2017.3 में तय किया गया


ArrayList से हटाना


 for (int i = from; i < to; i++) { list.remove(from); } 

समस्या List::remove विधि लागू करने में है:


 public E remove(int index) { Objects.checkIndex(index, size); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { System.arraycopy(array, index + 1, array, index, numMoved); // <---- } array[--size] = null; // clear to let GC do its work return oldValue; } 

समाधान:


 list.subList(from, to).clear(); 

लेकिन क्या होगा यदि स्रोत कोड में दूरस्थ मूल्य का उपयोग किया जाता है?


 for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed); } 

अब आपको पहले साफ की गई सूची से गुजरना होगा:


 List<String> removed = list.subList(from, to); removed.forEach(this::use); removed.clear(); 

यदि आप वास्तव में चक्र में हटाना चाहते हैं, तो सूची से गुजरने की दिशा में बदलाव से दर्द को कम करने में मदद मिलेगी। इसका अर्थ सेल की सफाई के बाद कम संख्या में तत्वों को स्थानांतरित करना है:


 //   , . .       for (int i = from; i < to; i++) { E removed = list.remove(from); use(removed, i); } //  , . .    for (int i = to - 1; i >= from; i--) { E removed = list.remove(i); use(removed, reverseIndex(i)); } 

सभी तीन तरीकों की तुलना करें (कॉलम के तहत 100 आइटम की सूची से 100% हटाए गए आइटम हैं):


जावा 8


जावा ९


वैसे, क्या किसी ने विसंगति को नोटिस किया?


देखने के लिए


यदि हम अंत से चल रहे सभी डेटा को हटा देते हैं, तो अंतिम तत्व हमेशा हटा दिया जाता है और कोई बदलाव नहीं होता है:


 // ArrayList public E remove(int index) { rangeCheck(index); modCount++; E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) { // <----     System.arraycopy(elementData, index+1, elementData, index, numMoved); } elementData[--size] = null; // clear to let GC do its work return oldValue; } 

बेंचमार्क


डेवलपर के लिए उपयोगी है

https://youtrack.jetbrains.com/issue/IDEA-177466
आईडिया-177,466 एक लूप में सूचीबद्ध List.remove (सूचकांक) का पता लगाएं
2018.2 में तय किया गया


निष्कर्ष: सिंगल ऑपरेशंस की तुलना में मास ऑपरेशन अक्सर तेज होते हैं।


स्कोप और प्रदर्शन


इस कोड को किसी विशेष स्पष्टीकरण की आवश्यकता नहीं है:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); List<Student> underAchieving = repository.findUnderAchieving(); // <---- if (settings.leaveBothCategories()) { leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

हम गुंजाइश को संकीर्ण करते हैं, जो शून्य से 1 क्वेरी देता है:


 void leaveForTheSecondYear() { List<Student> naughty = repository.findNaughty(); if (Settings.leaveBothCategories()) { List<Student> underAchieving = repository.findUnderAchieving(); // <---- leaveForTheSecondYear(naughty, underAchieving); // <---- return; } leaveForTheSecondYear(naughty); } 

और यहां चौकस पाठक को पूछना चाहिए: स्थैतिक विश्लेषण के बारे में क्या? सतह पर पड़े सुधार के बारे में आइडिया ने हमें क्यों नहीं बताया?


तथ्य यह है कि स्थिर विश्लेषण की संभावनाएं सीमित हैं: यदि विधि जटिल है (विशेष रूप से डेटाबेस के साथ बातचीत) और सामान्य स्थिति को प्रभावित करती है, तो इसके निष्पादन को स्थानांतरित करने से आवेदन टूट सकता है। स्थैतिक विश्लेषक बहुत सरल निष्पादन की रिपोर्ट करने में सक्षम है, जिसके हस्तांतरण का कहना है कि ब्लॉक के अंदर कुछ भी नहीं टूटेगा।


आप एक ersatz के रूप में चर हाइलाइटिंग का उपयोग कर सकते हैं, लेकिन फिर से, इसे सावधानी से उपयोग करें, क्योंकि दुष्प्रभाव हमेशा संभव हैं। आप स्टेटिक तरीकों को इंगित करने के लिए जेटब्रिंस-एनोटेशन लाइब्रेरी से उपलब्ध एनोटेशन @org.jetbrains.annotations.Contract(pure = true) उपयोग कर सकते हैं:


 // com.intellij.util.ArrayUtil @Contract(pure = true) public static int find(@NotNull int[] src, int obj) { return indexOf(src, obj); } 

निष्कर्ष: अधिक बार नहीं, अधिक काम केवल प्रदर्शन को खराब करता है।


सबसे असामान्य उदाहरण


 @Service public class RemoteService { private ContractCounter contractCounter; @Transactional(readOnly = true) // <---- public int countContracts(Dto dto) { if (dto.isInvalid()) { return -1; // <---- } return contractCounter.countContracts(dto); } } 

यह कार्यान्वयन लेन-देन की आवश्यकता होने पर भी लेनदेन खोलता है (विधि से त्वरित रिटर्न -1)।


आपको बस ContractCounter::countContracts अंदर की लेन-देन को दूर करना होगा, जहाँ इसकी आवश्यकता है, और इसे "बाहरी" विधि से हटा दें।


मामले के लिए निष्पादन समय की तुलना करें जब -1 (ns) लौटाया जाता है:


मेमोरी खपत (बाइट्स) की तुलना करें:


बेंचमार्क


निष्कर्ष: नियंत्रकों और "बाहरी" दिखने वाली सेवाओं को लेन-देन से मुक्त करने की आवश्यकता है (यह उनकी जिम्मेदारी नहीं है) और इनपुट डेटा सत्यापन के पूरे तर्क, जिसे डेटाबेस और लेन-देन के घटकों तक पहुंच की आवश्यकता नहीं है, को वहां से हटा दिया जाना चाहिए।


परिवर्तित दिनांक / समय स्ट्रिंग के लिए


शाश्वत कार्यों में से एक तारीख / समय को एक स्ट्रिंग में बदल रहा है। G8 से पहले, हमने यह किया:


 SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy"); String dateAsStr = formatter.format(date); 

JDK 8 की रिलीज़ के साथ, हमें LocalDate/LocalDateTime मिला और, तदनुसार, DateTimeFormatter


 DateTimeFormatter formatter = ofPattern("dd.MM.yyyy"); String dateAsStr = formatter.format(localDate); 

आइए इसके प्रदर्शन को मापें:


 Date date = new Date(); LocalDate localDate = LocalDate.now(); SimpleDateFormat sdf = new SimpleDateFormat("dd.MM.yyyy"); DateTimeFormatter dtf = DateTimeFormatter.ofPattern("dd.MM.yyyy"); @Benchmark public String simpleDateFormat() { return sdf.format(date); } @Benchmark public String dateTimeFormatter() { return dtf.format(localDate); } 

समय (सं।):


मेमोरी (बाइट्स):


प्रश्न: मान लीजिए कि हमारी सेवा को बाहर से डेटा प्राप्त होता है और हम java.util.Date मना नहीं कर सकते। क्या यह हमारे लिए फायदेमंद होगा कि Date को LocalDate परिवर्तित करें यदि बाद वाला तेजी से स्ट्रिंग में परिवर्तित हो जाए? आइए गिनती:


 @Benchmark public String measureDateConverted(Data data) { LocalDate localDate = toLocalDate(data.date); return data.dateTimeFormatter.format(localDate); } private LocalDate toLocalDate(Date date) { return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); } 

समय (सं।):


मेमोरी (बाइट्स):


इस प्रकार, "नौ" का उपयोग करते समय रूपांतरण Date -> LocalDate फायदेमंद है। G8 में, रूपांतरण की लागत DateTimeFormatter -a के सभी लाभों को DateTimeFormatter करेगी।


बेंचमार्क


निष्कर्ष: नए समाधानों का लाभ उठाएं।


एक और "आठ"


इस कोड में, हम स्पष्ट अतिरेक देखते हैं:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .collect(toList()) // <----    ? .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

हम इसे हटा देते हैं:


 Iterator<Long> iterator = items // ArrayList<Integer> .stream() .map(Long::valueOf) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

आइए देखें कि प्रदर्शन में कितना सुधार हुआ है:


नौ के साथ तुलना करें:


कमाल है ना? मैंने ऊपर तर्क दिया है कि अतिरिक्त कार्य प्रदर्शन को कम करता है। लेकिन यहां हम अतिरिक्त को हटा देते हैं - और (अचानक) यह खराब हो जाता है। यह समझने के लिए कि क्या हो रहा है, दो पुनरावृत्तियों को लें और उन्हें एक आवर्धक कांच के नीचे देखें:


पता चलता है
 Iterator iterator1 = items.stream().collect(toList()).iterator(); Iterator iterator2 = items.stream().iterator(); 


पहला पुनरावृत्त नियमित ArrayList$Itr


इसके माध्यम से मार्ग सरल है:
 public boolean hasNext() { return cursor != size; } public E next() { checkForComodification(); int i = cursor; if (i >= size) { throw new NoSuchElementException(); } Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) { throw new ConcurrentModificationException(); } cursor = i + 1; return (E) elementData[lastRet = i]; } 


दूसरा अधिक दिलचस्प है, यह Spliterators$Adapter , जो ArrayList$ArrayListSpliterator पर आधारित है।


वहां से गुजरना ज्यादा मुश्किल है
 // java.util.Spliterators$Adapter public boolean hasNext() { if (!valueReady) spliterator.tryAdvance(this); return valueReady; } public T next() { if (!valueReady && !hasNext()) throw new NoSuchElementException(); else { valueReady = false; return nextElement; } } 


आइए async-profiler के माध्यम से पुनरावृति पुनरावृत्ति को देखें:


 15.64% juArrayList$ArrayListSpliterator.tryAdvance 10.67% jusSpinedBuffer.clear 9.86% juSpliterators$1Adapter.hasNext 8.81% jusStreamSpliterators$AbstractWrappingSpliterator.fillBuffer 6.01% oojiBlackhole.consume 5.71% jusReferencePipeline$3$1.accept 5.57% jusSpinedBuffer.accept 5.06% cllbir.IteratorFromStreamBenchmark.iteratorFromStream 4.80% jlLong.valueOf 4.53% cllbiIteratorFromStreamBenchmark$$Lambda$8.885721577.apply 

यह देखा जा सकता है कि अधिकांश समय इट्रेटर से गुजरने में व्यतीत होता है, हालांकि और बड़े से, हमें इसकी आवश्यकता नहीं है, क्योंकि खोज इस तरह से की जा सकती है:


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

बाकी के साथ तुलना करें:


Stream::forEach स्पष्ट रूप से एक विजेता है, लेकिन यह अजीब है: यह अभी भी ArrayListSpliterator पर आधारित है, लेकिन इसके उपयोग में काफी सुधार हुआ है।


आइए प्रोफाइल देखें:
 29.04% oojiBlackhole.consume 22.92% juArrayList$ArrayListSpliterator.forEachRemaining 14.47% jusReferencePipeline$3$1.accept 8.79% jlLong.valueOf 5.37% cllbiIteratorFromStreamBenchmark$$Lambda$9.617691115.accept 4.84% cllbiIteratorFromStreamBenchmark$$Lambda$8.1964917002.apply 4.43% jusForEachOps$ForEachOp$OfRef.accept 4.17% jusSink$ChainedReference.end 1.27% jlInteger.longValue 0.53% jusReferencePipeline.map 

इस प्रोफाइल में, ज्यादातर समय Blackhole अंदर के मूल्यों को "निगलने" में बिताया जाता है। एक इटरेटर की तुलना में, समय का एक बड़ा हिस्सा सीधे जावा कोड के निष्पादन पर खर्च किया जाता है। यह माना जा सकता है कि कारण कचरा संग्रह का कम विशिष्ट भार है, इसकी तुलना इट्रेटर ब्रूट बल से की जाती है। की जाँच करें:


 forEach:·gc.alloc.rate.norm 100 avgt 30 216,001 ± 0,002 B/op iteratorFromStream:·gc.alloc.rate.norm 100 avgt 30 416,004 ± 0,006 B/op 

वास्तव में, Stream::forEach स्मृति की आधी खपत प्रदान करता है।


यह तेज क्यों है?

ब्लैक होल में शुरू से ही कॉल की श्रृंखला इस तरह दिखती है:



जैसा कि आप देख सकते हैं, ArrayListSpliterator::tryAdvance चेन से गायब हो गया, और ArrayListSpliterator::forEachRemaining दिखाई दी:


 // ArrayListSpliterator public void forEachRemaining(Consumer<? super E> action) { int i, hi, mc; // hoist accesses and checks from loop ArrayList<E> lst; Object[] a; if (action == null) throw new NullPointerException(); if ((lst = list) != null && (a = lst.elementData) != null) { if ((hi = fence) < 0) { mc = lst.modCount; hi = lst.size; } else mc = expectedModCount; if ((i = index) >= 0 && (index = hi) <= a.length) { for (; i < hi; ++i) { @SuppressWarnings("unchecked") E e = (E) a[i]; // <---- action.accept(e); } if (lst.modCount == mc) return; } } throw new ConcurrentModificationException(); } 

उच्च गति ArrayListSpliterator::forEachRemaining 1 विधि कॉल में संपूर्ण सरणी के माध्यम से पास का उपयोग करके प्राप्त किया जाता है। इटरेटर का उपयोग करते समय, मार्ग एक तत्व तक सीमित है, इसलिए हम हमेशा ArrayListSpliterator::tryAdvance खिलाफ आराम ArrayListSpliterator::tryAdvance
ArrayListSpliterator::forEachRemaining में पूरे सरणी तक पहुंच है और अतिरिक्त कॉल के बिना एक गिनती चक्र के साथ इस पर ArrayListSpliterator::forEachRemaining करता है।


महत्वपूर्ण सूचना

कृपया ध्यान दें कि यांत्रिक प्रतिस्थापन


 Iterator<Long> iterator = items .stream() .map(Long::valueOf) .collect(toList()) .iterator(); while (iterator.hasNext()) { bh.consume(iterator.next()); } 

पर


 items .stream() .map(Long::valueOf) .forEach(bh::consume); 

यह हमेशा समतुल्य नहीं होता है, क्योंकि पहले मामले में हम स्ट्रीम को प्रभावित किए बिना पास के लिए डेटा की एक प्रति का उपयोग करते हैं, और दूसरे मामले में डेटा को सीधे स्ट्रीम से लिया जाता है।


बेंचमार्क


निष्कर्ष: जब डेटा के जटिल अभ्यावेदन के साथ काम करते हैं, तो इस तथ्य के लिए तैयार रहें कि "लोहे" के नियम (अतिरिक्त कार्य हानि) काम करना बंद कर दें। ऊपर दिए गए उदाहरण से पता चलता है कि प्रतीत होता है कि बहुत ही शानदार मध्यवर्ती सूची गणना के तेजी से कार्यान्वयन का लाभ देती है।


दो टोटके


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays .asList(trace) .subList(0, depth) .toArray(new StackTraceElement[newDepth]); // <---- 

पहली चीज जो आपकी आंख को पकड़ती है, वह सड़ा हुआ "सुधार" है, अर्थात्, नॉनजरो की लंबाई की एक सरणी को Collection::toArray गुजरना। यह बहुत विस्तार से बताता है कि यह हानिकारक क्यों है।


दूसरी समस्या इतनी स्पष्ट नहीं है, और इसकी समझ के लिए हम समीक्षक और इतिहासकार के काम के बीच समानता ला सकते हैं।


इस बारे में रॉबिन कॉलिंगवुड लिखते हैं:

, , [] – . . , , ...


. :


1)
2)
3)


, :


 StackTraceElement[] trace = th.getStackTrace(); StackTraceElement[] newTrace = Arrays.copyOf(trace, depth); //    0 //  StackTraceElement[] newTrace = Arrays.copyOfRange(trace, 0, depth); //   0 

():




 List<T> list = getList(); Set<T> set = getSet(); return list.stream().allMatch(set::contains); //     ? 

, , :


 List<T> list = getList(); Set<T> set = getSet(); return set.containsAll(list); 

():




:


 interface FileNameLoader { String[] loadFileNames(); } 

:


 private FileNameLoader loader; void load() { for (String str : asList(loader.loadFileNames())) { // <----   use(str); } } 

, forEach , :


 private FileNameLoader loader; void load() { for (String str : loader.loadFileNames()) { // <----    use(str); } } 

():



https://youtrack.jetbrains.com/issue/IDEA-182206
IDEA-182206 Simplification for Arrays.asList().subList().toArray()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-180847
IDEA-180847 Inspection 'Call to Collection.toArray with zero-length array argument' brings pessimization
2018.1


https://youtrack.jetbrains.com/issue/IDEA-181928
IDEA-181928 Stream.allMatch(Collection::contains) can be simplified to Collection.containsAll()
2018.1


https://youtrack.jetbrains.com/issue/IDEA-184240
IDEA-184240 Unnecessary array-to-collection wrapping should be detected
2018.1


: :



, , , . , : "" ( ), "" ( ), .



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


All Articles