Merusak kinerja

Catatan ini adalah versi tertulis dari laporan saya "Cara merusak kinerja dengan kode yang tidak efisien" dari konferensi JPoint 2018. Anda dapat menonton video dan slide di halaman konferensi . Dalam jadwal, laporan ditandai dengan segelas smoothie ofensif, sehingga tidak akan ada yang super rumit, ini lebih cenderung untuk pemula.


Subjek laporan:


  • cara melihat kode untuk menemukan kemacetan di dalamnya
  • antipatterns umum
  • menyapu tidak jelas
  • menyapu memotong

Di sela-sela, mereka menunjukkan beberapa ketidakakuratan / kelalaian dalam laporan, mereka dicatat di sini. Komentar juga diterima.


Dampak Kinerja terhadap Kinerja


Ada kelas pengguna:


class User { String name; int age; } 

Kita perlu membandingkan objek satu sama lain, jadi kita mendeklarasikan metode equals dan hashCode :


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

Kode ini bisa diterapkan, pertanyaannya berbeda: apakah kinerja kode ini akan menjadi yang terbaik? Untuk menjawabnya, mari kita mengingat fitur dari metode Object::equals : ia mengembalikan hasil positif hanya ketika semua bidang yang dibandingkan adalah sama, jika tidak hasilnya akan negatif. Dengan kata lain, satu perbedaan sudah cukup untuk hasil negatif.


Setelah melihat kode yang dihasilkan untuk @EqualsAndHashCode kita akan melihat sesuatu seperti ini:


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

Urutan memeriksa bidang sesuai dengan urutan deklarasi mereka, yang dalam kasus kami bukan solusi terbaik, karena membandingkan objek menggunakan equals "lebih sulit" daripada membandingkan tipe sederhana.


Ok, mari kita coba membuat metode equals/hashCode menggunakan Idea:


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

Suatu Ide menciptakan kode yang lebih cerdas yang mengetahui kerumitan membandingkan berbagai jenis data. Nah, kita akan @EqualsAndHashCode dan kita akan secara eksplisit menulis equals/hashCode . Sekarang mari kita lihat apa yang terjadi ketika kelas diperluas:


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

equals/hashCode rekreasi 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); } 

Daftar dibandingkan sebelum string dibandingkan, yang tidak masuk akal ketika string berbeda. Pada pandangan pertama, tidak ada banyak perbedaan, karena string dengan panjang yang sama dibandingkan dengan tanda (yaitu, waktu perbandingan tumbuh bersama dengan panjang string):


Ada ketidakakuratan

Metode java.lang.String::equals adalah intrusif , jadi tidak ada perbandingan akses saat eksekusi.


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

Sekarang pertimbangkan untuk membandingkan dua ArrayList (sebagai implementasi daftar yang paling umum digunakan). Meneliti ArrayList , kami terkejut menemukan bahwa ia tidak memiliki implementasi yang equals , tetapi menggunakan implementasi yang diwarisi:



 //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()); } 

Penting di sini adalah penciptaan dua iterator dan berpasangan melalui mereka. Misalkan ada dua ArrayList :


  • dalam satu nomor dari 1 hingga 99
  • dalam angka kedua dari 1 hingga 100

Idealnya, akan cukup untuk membandingkan ukuran kedua daftar dan jika mereka tidak bersamaan, segera kembalikan hasil negatif (seperti yang dilakukan AbstractSet ), dalam kenyataannya, 99 perbandingan akan dilakukan dan hanya pada keseratus akan menjadi jelas bahwa daftar tersebut berbeda.


Ada apa dengan orang Kotlin?


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

Di sini semuanya seperti Lombok - urutan perbandingan sesuai dengan urutan pengumuman:


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

Bagaimana cara memperbaiki situasi? - mengeluh!

IDEA-170178 Perbandingan koleksi harus dieksekusi pada saat yang sama dengan yang dihasilkan oleh IDEA ()
https://youtrack.jetbrains.com/issue/IDEA-170178


Menyusun ulang perbandingan di @EqualsAndHashCode untuk kinerja yang lebih baik # 1543
https://github.com/rzwitserloot/lombok/issues/1543


KT-23184 yang dihasilkan secara otomatis sama dengan () dari kelas data tidak optimal
https://youtrack.jetbrains.com/issue/KT-23184


Sebagai solusinya, Anda bisa mengatur deklarasi lapangan secara manual.


Mari menyulitkan tugas


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

Kode ini melibatkan pengaksesan basis data bahkan ketika hasil dari pengecekan kondisi yang ditunjukkan oleh panah sudah dapat diprediksi sebelumnya. Jika nilai variabel yang valid salah, maka kode di blok if tidak akan pernah dijalankan, yang berarti Anda bisa melakukannya tanpa permintaan:


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

Catatan dari sela-sela

Sinking bisa tidak signifikan ketika entitas kembali dari JpaRepository::findOne sudah ada di cache tingkat pertama - maka tidak akan ada permintaan.


Contoh serupa tanpa bercabang eksplisit:


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

Pengembalian cepat memungkinkan Anda untuk menunda permintaan:


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

Tambahan yang cukup jelas yang tidak muncul dalam laporan

Bayangkan bahwa cek tertentu menggunakan entitas yang serupa:


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

Jika pemeriksaan menggunakan entitas yang sama, maka Anda harus memastikan bahwa panggilan ke entitas / koleksi anak "malas" dilakukan setelah panggilan ke bidang yang sudah dimuat. Pada pandangan pertama, satu permintaan tambahan tidak akan memiliki dampak signifikan pada keseluruhan gambar, tetapi semuanya bisa berubah ketika suatu tindakan dilakukan dalam satu lingkaran.


Kesimpulan: rantai tindakan / pemeriksaan harus dipesan dalam rangka meningkatkan kompleksitas operasi individu, mungkin beberapa dari mereka tidak harus dilakukan.


Siklus dan Pemrosesan Massal


Contoh berikut tidak perlu penjelasan khusus:


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

Karena beberapa permintaan basis data, kode ini lambat.


Komentar

Kinerja dapat tenggelam lebih banyak lagi jika metode enrollStudents dijalankan di luar transaksi: maka setiap panggilan ke osdjrJpaRepository::findOne akan dieksekusi dalam transaksi baru (lihat SimpleJpaRepository ), yang berarti menerima dan mengembalikan koneksi ke database, serta membuat dan membilas cache tingkat pertama.


Perbaiki:


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

Mari kita ukur runtime (dalam mikrodetik) untuk koleksi kunci (10 dan 100 buah)


Tolok ukur


Komentar

Jika Anda menggunakan Oracle dan memberikan lebih dari 1000 kunci untuk menemukanAll, maka Anda akan mendapatkan pengecualian ORA-01795: maximum number of expressions in a list is 1000 .
Juga, melakukan tugas yang berat (dengan banyak tombol) in kueri mungkin lebih buruk daripada n kueri. Itu semua tergantung pada aplikasi spesifik, sehingga penggantian mekanis dari siklus ke pemrosesan massal dapat menurunkan kinerja.


Contoh yang lebih kompleks tentang topik yang sama


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

Dalam kasus ini, kami tidak dapat mengganti loop dengan JpaRepository::findAll , JpaRepository::findAll ini akan mematahkan logika: semua nilai yang diperoleh dari JpaRepository::findAll tidak akan menjadi null dan blok if tidak akan berfungsi.


Fakta bahwa untuk setiap kunci basis data akan membantu kami mengatasi kesulitan ini
mengembalikan nilai aktual atau ketidakhadirannya. Dalam arti tertentu, basis data adalah kamus. Java dari kotak memberi kami implementasi kamus yang sudah jadi - HashMap - di atasnya kami akan membangun logika untuk mengganti database:


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

Membalikkan contoh


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

Kode ini selalu membuat transaksi baru untuk menyimpan daftar entitas. Kendor dimulai dengan beberapa panggilan ke metode yang membuka transaksi baru:


 //   @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); } 

Solusi: terapkan metode Saver::save segera untuk seluruh kumpulan data:


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

Banyak transaksi bergabung menjadi satu, yang memberikan peningkatan nyata (waktu dalam mikrodetik):


Tolok ukur


Contoh dengan beberapa transaksi sulit diformalkan, yang tidak dapat dikatakan tentang memanggil JpaRepository::findOne dalam satu lingkaran.


Karena itu, kami akan mengambil tindakan

https://youtrack.jetbrains.com/issue/IDEA-165730
IDEA-165730 Peringatkan tentang penggunaan JpaRepository yang tidak efisien


https://youtrack.jetbrains.com/issue/IDEA-165942
IDEA-165942 Inspeksi untuk mengganti pemanggilan metode dalam satu lingkaran dengan operasi massal
Diperbaiki pada 2017.1


Pendekatan ini tidak hanya berlaku untuk basis data, jadi Tagir lany Valeev melangkah lebih jauh. Dan jika sebelumnya kita menulis seperti ini:


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

dan semuanya baik-baik saja, sekarang "Ide" menyarankan untuk memperbaiki dirinya sendiri:


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

Tetapi bahkan opsi ini tidak selalu memuaskannya, karena Anda dapat membuatnya lebih pendek dan lebih cepat:


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

Bandingkan (waktu dalam ns)

Untuk ArrayList, peningkatan ini memberikan peningkatan yang nyata:



Untuk HashSet, itu tidak begitu cerah:



Tolok ukur


Berguna untuk pengembang:

https://youtrack.jetbrains.com/issue/IDEA-138456
IDEA-138456 Inspeksi baru: Collection.addAll () dapat diganti dengan konstruktor parametrized
Diperbaiki pada 142.1217


https://youtrack.jetbrains.com/issue/IDEA-178761
IDEA-178761 Inspeksi 'Collection.addAll () dapat diganti dengan konstruktor parametrized' harus diaktifkan secara default
Diperbaiki pada 2017.3


Menghapus dari ArrayList


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

Masalahnya adalah dalam mengimplementasikan metode 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; } 

Solusi:


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

Tetapi bagaimana jika nilai jarak jauh digunakan dalam kode sumber?


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

Sekarang Anda harus melalui daftar yang dibersihkan terlebih dahulu:


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

Jika Anda benar-benar ingin menghapus dalam siklus, maka perubahan dalam arah perjalanan melalui daftar akan membantu meringankan rasa sakit. Artinya adalah untuk menggeser sejumlah kecil elemen setelah membersihkan sel:


 //   , . .       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)); } 

Bandingkan ketiga metode (di bawah kolom adalah% item yang dihapus dari daftar ukuran 100):


Java 8


Jawa 9


Ngomong-ngomong, apakah seseorang memperhatikan anomali?


Untuk melihat


Jika kami menghapus setengah dari semua data yang bergerak dari ujung, maka elemen terakhir selalu dihapus dan tidak ada perubahan:


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

Tolok ukur


Berguna untuk pengembang

https://youtrack.jetbrains.com/issue/IDEA-177466
IDEA-177466 Detect List.remove (index) dipanggil dalam satu lingkaran
Diperbaiki pada 2018.2


Kesimpulan: operasi massal seringkali lebih cepat daripada operasi tunggal.


Lingkup dan kinerja


Kode ini tidak memerlukan penjelasan khusus:


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

Kami mempersempit ruang lingkup, yang memberikan kueri minus 1:


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

Dan di sini pembaca yang penuh perhatian harus bertanya: bagaimana dengan analisis statis? Mengapa Ide tidak memberi tahu kami tentang peningkatan yang terjadi di permukaan?


Faktanya adalah bahwa kemungkinan analisis statis terbatas: jika metode ini kompleks (terutama berinteraksi dengan database) dan mempengaruhi keadaan umum, maka mentransfer pelaksanaannya dapat merusak aplikasi. Analyzer statis dapat melaporkan eksekusi yang sangat sederhana, yang transfernya, katakanlah, di dalam blok tidak akan merusak apa pun.


Anda dapat menggunakan penyorotan variabel sebagai variabel, tetapi sekali lagi, gunakan dengan hati-hati, karena efek samping selalu mungkin. Anda dapat menggunakan anotasi @org.jetbrains.annotations.Contract(pure = true) , tersedia dari perpustakaan jetbrains-annotations untuk menunjukkan metode stateless :


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

Kesimpulan: lebih sering daripada tidak, kerja berlebihan hanya memengaruhi kinerja.


Contoh paling tidak biasa


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

Implementasi ini membuka transaksi bahkan ketika transaksi tidak diperlukan (pengembalian cepat -1 dari metode).


Yang perlu Anda lakukan adalah menghapus transaksionalitas di dalam metode ContractCounter::countContracts , di mana diperlukan, dan menghapusnya dari metode "eksternal".


Bandingkan waktu eksekusi untuk kasus ketika -1 (ns) dikembalikan:


Bandingkan konsumsi memori (byte):


Tolok ukur


Kesimpulan: pengontrol dan layanan yang tampak "keluar" perlu dibebaskan dari transaksionalitas (ini bukan tanggung jawab mereka) dan seluruh logika verifikasi data input, yang tidak memerlukan akses ke database dan komponen transaksional, harus diambil di sana.


Ubah tanggal / waktu menjadi string


Salah satu tugas abadi adalah mengubah tanggal / waktu menjadi string. Sebelum G8, kami melakukan ini:


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

Dengan rilis JDK 8, kami mendapat LocalDate/LocalDateTime dan, karenanya, DateTimeFormatter


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

Mari kita ukur kinerjanya:


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

Waktu:


Memori (byte):


Pertanyaan: misalkan layanan kami menerima data dari luar dan kami tidak bisa menolak java.util.Date . Apakah bermanfaat bagi kita untuk mengonversi Date ke LocalDate jika yang terakhir lebih cepat dikonversi ke string? Hitung:


 @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(); } 

Waktu:


Memori (byte):


Dengan demikian, Date konversi -> LocalDate bermanfaat saat menggunakan "sembilan". Di G8, biaya konversi akan melahap semua manfaat DateTimeFormatter -a.


Tolok ukur


Kesimpulan: manfaatkan solusi baru.


"Delapan" lainnya


Dalam kode ini, kita melihat redundansi yang jelas:


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

Kami menghapusnya:


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

Mari kita lihat seberapa banyak peningkatan kinerja:


Bandingkan dengan sembilan:


Luar biasa bukan? Saya berpendapat di atas bahwa kelebihan kerja menurunkan kinerja. Tapi di sini kita menghapus kelebihan - dan (tiba-tiba) semakin buruk. Untuk memahami apa yang terjadi, ambil dua iterator dan lihatlah di bawah kaca pembesar:


Diungkapkan
 Iterator iterator1 = items.stream().collect(toList()).iterator(); Iterator iterator2 = items.stream().iterator(); 


Iterator pertama adalah ArrayList$Itr .


Bagian yang melaluinya sederhana:
 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]; } 


Yang kedua lebih menarik, itu adalah Spliterators$Adapter , yang didasarkan pada ArrayList$ArrayListSpliterator .


Melewati itu lebih sulit
 // 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; } } 


Mari kita lihat iterator iteration melalui 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 

Dapat dilihat bahwa sebagian besar waktu dihabiskan untuk melewati iterator, meskipun pada umumnya, kita tidak membutuhkannya, karena pencarian dapat dilakukan seperti ini:


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

Bandingkan dengan yang lain:


Stream::forEach jelas merupakan pemenang, tetapi ini aneh: masih berdasarkan ArrayListSpliterator , tetapi penggunaannya telah meningkat secara signifikan.


Mari kita lihat profilnya:
 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 

Dalam profil ini, sebagian besar waktu dihabiskan untuk "menelan" nilai-nilai di dalam Blackhole . Dibandingkan dengan iterator, sebagian besar waktu dihabiskan secara langsung untuk mengeksekusi kode Java. Dapat diasumsikan bahwa alasannya adalah berat spesifik yang lebih rendah dari pengumpulan sampah, dibandingkan dengan kekuatan kasar iterator. Periksa:


 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 

Memang, Stream::forEach menyediakan setengah konsumsi memori.


Kenapa lebih cepat?

Rantai panggilan dari awal hingga lubang hitam terlihat seperti ini:



Seperti yang Anda lihat, panggilan ke ArrayListSpliterator::tryAdvance menghilang dari rantai, dan ArrayListSpliterator::forEachRemaining muncul 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 kecepatan tinggi ArrayListSpliterator::forEachRemaining dicapai dengan menggunakan seluruh array dalam 1 metode panggilan. Saat menggunakan iterator, bagian ini terbatas pada satu elemen, jadi kami selalu bersandar pada ArrayListSpliterator::tryAdvance .
ArrayListSpliterator::forEachRemaining memiliki akses ke seluruh array dan ArrayListSpliterator::forEachRemaining dengan siklus penghitungan tanpa panggilan tambahan.


Pemberitahuan penting

Harap dicatat bahwa penggantian mekanik


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

pada


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

Itu tidak selalu setara, karena dalam kasus pertama kami menggunakan salinan data untuk bagian tanpa mempengaruhi aliran itu sendiri, dan dalam kasus kedua data diambil langsung dari aliran.


Tolok ukur


Kesimpulan: ketika berhadapan dengan representasi data yang kompleks, bersiaplah untuk fakta bahwa bahkan aturan "besi" (bahaya kerja ekstra) berhenti bekerja. Contoh di atas menunjukkan bahwa daftar perantara yang tampaknya berlebihan memberikan keuntungan dari pelaksanaan enumerasi yang lebih cepat.


Dua trik


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

Hal pertama yang menarik perhatian Anda adalah "perbaikan" busuk, yaitu, melewatkan array dengan panjang nol ke metode Collection::toArray . Ini menjelaskan dengan sangat terperinci mengapa ini berbahaya.


Masalah kedua tidak begitu jelas, dan untuk pemahamannya kita dapat menarik paralel antara karya pengkaji dan sejarawan.


Inilah yang ditulis Robin Collingwood tentang ini:

, , [] โ€“ . . , , ...


. :


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


All Articles