أداء الخراب

هذه الملاحظة هي نسخة مكتوبة من تقريري "كيفية إتلاف الأداء باستخدام كود غير فعال" من مؤتمر JPoint 2018. يمكنك مشاهدة مقاطع الفيديو والشرائح على صفحة المؤتمر . في الجدول ، تم وضع علامة على التقرير بكوب هجومي من العصائر ، لذلك لن يكون هناك أي شيء معقد للغاية ، هذا أكثر احتمالًا للمبتدئين.


موضوع التقرير:


  • كيفية إلقاء نظرة على الرمز للعثور على الاختناقات فيه
  • مضادات الاكتئاب الشائعة
  • أشعل النار غير واضحة
  • أشعل النار تجاوز

على الهامش ، أشاروا إلى بعض عدم الدقة / السهو في التقرير ، وقد لوحظوا هنا. التعليقات هي أيضا موضع ترحيب.


تأثير الأداء على الأداء


هناك فئة مستخدم:


class User { String name; int age; } 

نحن بحاجة إلى مقارنة الكائنات مع بعضها البعض ، لذلك نعلن عن طرق equals و hashCode :


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

الكود قابل للتطبيق ، والسؤال مختلف: هل سيكون أداء هذا الكود هو الأفضل؟ للإجابة عليه ، دعنا نتذكر ميزات أسلوب 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 صراحة equals/hashCode . الآن دعونا نرى ما يحدث عندما يمتد الفصل:


 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 مقارنة ، وفي المائة فقط سوف يتضح أن القوائم مختلفة.


ماذا يوجد مع Kotlinites؟


 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


إعادة ترتيب المقارنة فيEqualsAndHashCode لأداء أفضل # 1543
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 بالفعل في ذاكرة التخزين المؤقت للمستوى الأول - فلن يكون هناك أي طلب.


مثال مشابه بدون تفرع صريح:


 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 الأداء بشكل أكبر إذا enrollStudents تنفيذ طريقة التسجيل enrollStudents خارج المعاملة: ثم سيتم تنفيذ كل استدعاء لـ osdjrJpaRepository::findOne في معاملة جديدة (انظر SimpleJpaRepository ) ، مما يعني تلقي اتصال بقاعدة البيانات وإعادته ، بالإضافة إلى إنشاء ذاكرة التخزين المؤقت للمستوى الأول وإعادتها.


الإصلاح:


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

دعنا نقيس وقت التشغيل (بالميكروثانية) لمجموعة من المفاتيح (10 و 100 قطعة)


المعيار


ملاحظة

إذا كنت تستخدم Oracle ومررت أكثر من 1000 مفتاح findAll على findAll ، فستحصل على الاستثناء ORA-01795: maximum number of expressions in a list is 1000 .
أيضًا ، قد يكون أداء الاستعلامات الثقيلة (مع العديد من المفاتيح) أسوأ من n الاستعلامات. كل هذا يتوقف على التطبيق المحدد ، لذلك يمكن أن يؤدي الاستبدال الميكانيكي للدورة إلى المعالجة الجماعية إلى تدهور الأداء.


مثال أكثر تعقيدًا حول نفس الموضوع


 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 لن تكون 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 الفور لمجموعة البيانات بالكامل:


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

يتم دمج الكثير من المعاملات في صفقة واحدة ، مما يعطي زيادة ملموسة (الوقت بالميكروثانية):


المعيار


من الصعب إضفاء الطابع الرسمي على مثال للمعاملات المتعددة ، والذي لا يمكن قوله عن استدعاء JpaRepository::findOne في حلقة.


لذلك ، سنتخذ تدابير

https://youtrack.jetbrains.com/issue/IDEA-165730
IDEA-165730 تحذير من الاستخدام غير الفعال لـ JpaRepository


https://youtrack.jetbrains.com/issue/IDEA-165942
IDEA-165942 التفتيش لاستبدال استدعاء الأسلوب في حلقة بعملية مجمعة
ثابت في 2017.1


لا ينطبق هذا النهج على قاعدة البيانات فقط ، لذلك ذهب Tagir lany Valeev إلى أبعد من ذلك. وإذا كتبنا سابقًا مثل هذا:


 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 ، يعطي هذا التحسين زيادة ملحوظة:



بالنسبة لـ HashSet ، إنها ليست وردية:



المعيار


مفيد للمطور:

https://youtrack.jetbrains.com/issue/IDEA-138456
IDEA-138456 فحص جديد: Collection.addAll () يمكن استبداله بمنشئ بارامتر
تم الإصلاح في 142.1217


https://youtrack.jetbrains.com/issue/IDEA-178761
IDEA-178761 يجب تشغيل الفحص '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):


جافا 8


جافا 9


بالمناسبة ، هل لاحظ أحدهم الشذوذ؟


لنرى


إذا قمنا بحذف نصف جميع البيانات التي تنتقل من النهاية ، فسيتم حذف العنصر الأخير دائمًا وليس هناك إزاحة:


 // 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
IDEA-177466 كشف قائمة. إزالة (فهرس) في حلقة
تم الإصلاح في 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); } 

وهنا يجب أن يسأل القارئ اليقظ: ماذا عن التحليل الساكن؟ لماذا لم تخبرنا Idea عن التحسن في الاستلقاء على السطح؟


والحقيقة هي أن إمكانيات التحليل الثابت محدودة: إذا كانت الطريقة معقدة (خاصة التفاعل مع قاعدة البيانات) وتؤثر على الحالة العامة ، فإن نقل تنفيذها يمكن أن يكسر التطبيق. المحلل الساكن قادر على الإبلاغ عن عمليات الإعدام البسيطة للغاية ، ولن يؤدي نقلها ، على سبيل المثال ، داخل الكتلة إلى كسر أي شيء.


يمكنك استخدام إبراز متغير كإرساتز ، ولكن مرة أخرى ، استخدمه بعناية ، لأن الآثار الجانبية ممكنة دائمًا. يمكنك استخدام التعليق التوضيحي @org.jetbrains.annotations.Contract(pure = true) ، المتوفر من مكتبة التعليقات التوضيحية jetbrains للإشارة إلى الأساليب عديمة الحالة :


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


المعيار


الخلاصة: الاستفادة من الحلول الجديدة.


آخر "ثمانية"


في هذا الكود ، نرى تكرارًا واضحًا:


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


دعونا نلقي نظرة على تكرار المكرر من خلال المحلل المتزامن :


 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 الواضح أن 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 . مقارنة بالمكرر ، يتم إنفاق جزء أكبر بكثير من الوقت مباشرة على تنفيذ كود Java. يمكن افتراض أن السبب هو انخفاض الوزن المحدد لمجموعة القمامة ، مقارنةً بالقوة الغاشمة المكررة. تحقق:


 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 تحقيق ArrayListSpliterator::forEachRemaining عالي السرعة باستخدام تمرير عبر الصفيف بأكمله في استدعاء أسلوب 1. عند استخدام مكرر ، يقتصر المقطع على عنصر واحد ، لذلك فنحن نرتاح دائمًا ضد 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 هو "تحسن" فاسد ، أي تمرير مجموعة من الطول غير الصفري إلى أسلوب 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/ar423305/


All Articles