Tidak semua tambalan sama-sama bermanfaat.

Posting ini melanjutkan diskusi tentang peningkatan kinerja yang dapat menjadi kenyataan jika bukan karena perbedaan tetapi. Bagian sebelumnya tentang StringBuilder ada di sini .


Di sini kita melihat beberapa "perbaikan" ditolak karena kurangnya pemahaman tentang seluk-beluk spesifikasi bahasa, kasus sudut tidak jelas, dan alasan lainnya. Ayo pergi!


Ketika tidak ada yang menandakan masalah


Saya pikir kita masing-masing bekerja dengan metode Collections.emptySet() / Collections.emptyList() . Ini adalah metode yang sangat berguna yang memungkinkan Anda untuk mengembalikan koleksi kosong yang kosong tanpa membuat objek baru. Melihat ke dalam kelas EmptyList kita akan melihat ini:


 private static class EmptyList<E> { public Iterator<E> iterator() { return emptyIterator(); } public Object[] toArray() { return new Object[0]; } } 

Lihat potensi kuat untuk peningkatan? Metode EmptyList.iterator() mengembalikan iterator kosong dari hadapan, mengapa tidak membuat tipuan yang sama dengan telinga Anda untuk array yang dikembalikan oleh metode toArray() ?


Ada satu tapi, dan itu disebut dokumentasi
Array yang dikembalikan akan "aman" karena tidak ada referensi yang dipertahankan oleh daftar ini. (Dengan kata lain, metode ini harus mengalokasikan array baru meskipun daftar ini didukung oleh array). Penelepon bebas untuk memodifikasi array yang dikembalikan.

http://mail.openjdk.java.net/pipermail/core-libs-dev/2017-September/049170.html
http://hg.openjdk.java.net/jdk/jdk12/file/ffac5eabbf28/src/java.base/share/classes/java/util/List.java#l185


Dengan kata lain, metode ini harus selalu mengembalikan array baru .


Anda akan berkata: "Dia tidak bisa diubah! Apa yang salah?"


Hanya ahli yang berpengalaman yang dapat menjawab pertanyaan ini:


- Siapa yang bertanggung jawab?
- Pakar yang bertanggung jawab Paul Sandoz dan Tagir Valeev

Jawaban para ahli

http://mail.openjdk.java.net/pipermail/core-libs-dev/2017-September/049171.html


Perhatikan juga bahwa ini mengubah perilaku yang terlihat. E. g. seseorang mungkin menyinkronkan objek array yang dikembalikan oleh panggilan toArray, jadi perubahan ini dapat menyebabkan berbagi kunci yang tidak diinginkan.

Setelah saya menyarankan perbaikan serupa: untuk mengembalikan EMPTY_LIST dari Arrays.asList () ketika array yang disediakan memiliki panjang nol. Itu ditolak dengan alasan yang sama [1].

http://mail.openjdk.java.net/pipermail/core-libs-dev/2015-September/035197.html

Ngomong-ngomong, mungkin masuk akal jika bagi Arrays.asList untuk memeriksa panjang array seperti:
 public static <T> List<T> asList(T... a) { if(a.length == 0) return Collections.emptyList(); return new ArrayList<>(a); } 


Kedengarannya masuk akal, bukan? Mengapa membuat daftar baru untuk array kosong jika Anda dapat mengambil yang sudah jadi secara gratis?
Ada alasan untuk tidak melakukan ini. Saat ini Arrays.asList tidak menetapkan batasan pada identitas Daftar yang dikembalikan. Menambahkan optimasi mikro akan mengubahnya. Ini adalah kasus tepi dan kasus penggunaan yang dipertanyakan juga, tetapi mengingat bahwa saya akan secara konservatif meninggalkan hal-hal sebagaimana adanya.

Pernyataan ini mungkin akan menyebabkan Anda bingung:


E. g. seseorang mungkin menyinkronkan objek array yang dikembalikan oleh panggilan toArray, jadi perubahan ini dapat menyebabkan berbagi kunci yang tidak diinginkan.

Anda akan berkata: "Siapa yang waras akan disinkronkan pada array (!) Dikembalikan (!!!) dari koleksi!?"


Kedengarannya tidak terlalu bisa dipercaya, tetapi bahasa tersebut memberikan peluang seperti itu, yang berarti ada kemungkinan pengguna tertentu akan melakukan ini (atau bahkan sudah melakukannya). Kemudian perubahan yang diajukan akan mengubah perilaku kode, dan paling buruk, itu akan menyebabkan gangguan dalam sinkronisasi (lanjutkan, tangkap). Risiko itu sangat tidak bisa dibenarkan, dan keuntungan yang diharapkan sangat kecil sehingga lebih baik meninggalkan semuanya apa adanya.


Secara umum, kemampuan untuk melakukan sinkronisasi pada objek apa pun, kmk, adalah kesalahan pengembang bahasa. Pertama, tajuk setiap objek berisi struktur yang bertanggung jawab untuk sinkronisasi, dan kedua, kita menemukan diri kita dalam situasi yang dijelaskan di atas ketika objek yang tampaknya tidak dapat dikembalikan beberapa kali, karena dapat disinkronkan.


Moral dari dongeng ini adalah ini: spesifikasi dan kompatibilitas mundur adalah sapi suci Jawa. Jangan coba-coba merambahnya: penjaga menembak tanpa peringatan.


Mencoba, mencoba ...


Ada beberapa kelas berbasis array di JDK sekaligus, dan semuanya menerapkan metode List.indexOf() dan List.lastIndexOf() :


  • java.util.ArrayList
  • java.util.Arrays $ ArrayList
  • java.util.Vector
  • java.util.concurrent.CopyOnWriteArrayList

Kode metode ini di kelas-kelas ini diulang hampir satu lawan satu. Banyak aplikasi dan kerangka kerja juga menawarkan solusi mereka untuk masalah yang sama:



Akibatnya, kami memiliki kode nyasar yang perlu dikompilasi (kadang-kadang kadang beberapa kali), yang terjadi di ReserverCodeCache, yang perlu diuji, dan yang hanya berkeliaran dari kelas ke kelas tanpa perubahan.


Pengembang, pada gilirannya, sangat suka menulis sesuatu seperti


 int i = Arrays.asList(array).indexOf(obj); //  boolean contains = Arrays.asList(array).contains(obj); //    boolean contains = Arrays.stream(names).anyMatch(nm -> nm.equals(name)); 

Saya ingin memperkenalkan metode utilitas umum di JDK, dan menggunakannya di mana-mana, seperti yang disarankan . Tambalannya sesederhana dua uang:


1) implementasi List.indexOf() dan List.lastIndexOf() pindah ke java.util.Arrays
2) sebagai gantinya, Arrays.indexOf() dan Arrays.lastIndexOf() dipanggil, masing-masing


Tampaknya apa yang salah? Keuntungan dari pendekatan ini [tampaknya] jelas. Tetapi artikel itu tentang kegagalan, jadi pikirkan apa yang salah.


- Siapa yang bertanggung jawab?
- Pakar yang bertanggung jawab Martin Buchholz dan Paul Sandoz

IMHO, sedikit kencang, tapi tetap saja

Martin Buchholz:


Sergey, saya semacam mempertahankan semua kelas koleksi itu, dan kadang-kadang saya juga ingin memiliki metode indexOf di Array.java. Tapi:

Array umumnya tidak disarankan. Setiap metode statis baru pada Array (atau, di mana saya benar-benar menginginkannya, pada objek array itu sendiri! Membutuhkan perubahan bahasa java!) Akan menemui perlawanan.

Kami menyesal menyesali null dalam koleksi, jadi kelas koleksi baru seperti ArrayDeque tidak mendukungnya.

Varian lain yang mungkin diinginkan pengguna adalah perbandingan kesetaraan seperti apa yang digunakan.

Kami menyesal memiliki ArrayList dengan indeks awal berbasis nol - akan lebih baik untuk memiliki perilaku array array ArrayDeque sejak hari pertama.

Kode untuk mencari irisan array sangat kecil, jadi Anda tidak banyak menghemat. Sangat mudah untuk membuat kesalahan off-by-one, tetapi itu berlaku untuk API Array juga.

Paul Sandoz:


Saya tidak akan pergi sejauh mengatakan array tidak disarankan, saya positif akan memutarnya sebagai "gunakan dengan hati-hati" karena mereka berduri misalnya selalu bisa berubah. Mereka pasti bisa ditingkatkan. Saya akan sangat senang melihat array mengimplementasikan array yang umum 'Antarmuka ish, kita mungkin dapat membuat beberapa kemajuan setelah sedimen tipe nilai.

Setiap penambahan baru pada Array akan menemui beberapa perlawanan, setidaknya bagi saya :-) Ini tidak pernah menambahkan hanya satu atau dua metode, banyak yang lain ingin ikut dalam perjalanan juga (semua primitif ditambah varian rentang). Jadi setiap fitur baru harus cukup menguntungkan dan dalam hal ini saya tidak berpikir manfaatnya cukup kuat (seperti tekanan cache kode reduksi yang mungkin).

Paul.


Korespondensi: http://mail.openjdk.java.net/pipermail/core-libs-dev/2018-March/051968.html


Moral dari dongeng ini adalah ini: tambalan Anda yang cerdik dapat dibantai untuk diperiksa hanya karena mereka tidak akan melihat nilai khusus di dalamnya. Ya, ada kode duplikat, tetapi tidak mengganggu siapa pun, jadi biarkan itu hidup.


Perbaikan untuk ArrayList? Saya memilikinya


Moped tambalan itu bukan milik saya, saya hanya akan mempostingnya untuk Anda pikirkan. Proposal itu sendiri disuarakan di sini , dan itu terlihat sangat menarik. Lihat sendiri:


Usulan Perubahan

Dengan mata telanjang, proposal itu sangat, sangat logis. Anda dapat mengukur kinerja menggunakan tolok ukur sederhana:


 @State(Scope.Benchmark) public class ArrayListBenchmark { @State(Scope.Benchmark) public static class Data { @Param({"10", "100", "1000", "10000"}) public int size; ArrayList<Integer> arrayRandom = new ArrayList<Integer>(size); @Setup(Level.Invocation) public void initArrayList() { Random rand = new Random(); rand.setSeed(System.currentTimeMillis()); // Populate the ArrayList with size-5 elements for (int i = 0; i < size - 5; i++) { Integer r = rand.nextInt() % 256; arrayRandom.add(r); } } } @Benchmark public ArrayList construct_new_array_list(Data d) { ArrayList al = new ArrayList(d.arrayRandom); // once a new ArrayList is created add a new element al.add(new Integer(900)); return al; } } 

Ringkasan:


 Benchmark (size) Mode Cnt Score Error Units  construct_new_array_list 10 thrpt 25 388.212 ± 23.110 ops/s construct_new_array_list 100 thrpt 25 90.208 ± 7.995 ops/s construct_new_array_list 1000 thrpt 25 23.289 ± 1.687 ops/s construct_new_array_list 10000 thrpt 25 7.659 ± 0.560 ops/s  construct_new_array_list 10 thrpt 25 562.678 ± 37.370 ops/s construct_new_array_list 100 thrpt 25 119.791 ± 13.232 ops/s construct_new_array_list 1000 thrpt 25 33.811 ± 3.812 ops/s construct_new_array_list 10000 thrpt 25 10.889 ± 0.564 ops/s 

Tidak buruk sama sekali untuk perubahan sederhana. Yang utama adalah sepertinya tidak ada tangkapan. Jujur buat sebuah array, jujur ​​salin data dan jangan lupa tentang ukurannya. Sekarang mereka pasti harus menerima tambalan!


Tapi itu dia

Martin Buchholz:


Ini bukan pertanyaan bahwa kita dapat mengoptimalkan kasus ArrayList -> ArrayList, tetapi bagaimana dengan semua implementasi Koleksi lainnya? ArrayDeque dan CopyOnWriteArrayList datang ke pikiran.
ArrayList adalah kelas populer yang digunakan untuk membuat salinan Koleksi. Di mana Anda berhenti?

Subclass patologis dari ArrayList dapat memutuskan untuk tidak menyimpan elemen dalam array dukungan, dengan kerusakan berikutnya.

Solusi terberkati untuk masalah penyalinan daftar mungkin adalah List.copyOf https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/List.html#copyOf(java .util.Collection ) yang mungkin melakukan optimasi yang Anda harapkan.

Alan Bateman


ArrayList belum final sehingga mungkin seseorang telah memperluasnya untuk menggunakan sesuatu selain elementData. Mungkin lebih aman untuk menggunakan identitas kelas daripada instanceof.

Tidak ada yang melarang saya untuk keluar dari ArrayList , dan untuk menyimpan data dalam daftar tertaut. Kemudian c instanceof ArrayList akan mengembalikan kebenaran, kita akan sampai ke area copy dan jatuh dengan aman.


Moral dari dongeng ini adalah ini: kemungkinan perubahan perilaku dapat menjadi penyebab kegagalan. Dengan kata lain, seseorang harus mengingat kemungkinan bahkan perubahan yang paling tidak masuk akal, jika diizinkan dengan cara bahasa. Dan ya, itu bisa berhasil jika ArrayList telah menyatakan final dari awal.


Spesifikasi lagi


Saat debug aplikasi saya, saya tidak sengaja jatuh ke nyali Spring dan menemukan kode berikut:


 //      paramTypes for (Constructor<?> candidate : candidates) { Class<?>[] paramTypes = candidate.getParameterTypes(); if (constructorToUse != null && argsToUse != null && argsToUse.length > paramTypes.length) { // Already found greedy constructor that can be satisfied -> // do not look any further, there are only less greedy constructors left. break; } if (paramTypes.length < minNrOfArgs) { continue; } 

Untungnya, dengan masuk ke dalam java.lang.reflect.Constructor.getParameterTypes() saya menggulirkan kode sedikit lebih rendah dan menemukan yang indah:


 @Override public Class<?>[] getParameterTypes() { return parameterTypes.clone(); } /** * {@inheritDoc} * @since 1.8 */ public int getParameterCount() { return parameterTypes.length; } 

Anda mengerti, ya? Jika kita perlu mengetahui jumlah argumen konstruktor / metode, maka panggil saja java.lang.reflect.Method.getParameterCount() dan lakukan tanpa menyalin array. Periksa apakah game bernilai lilin dalam kasus paling sederhana, di mana metode ini tidak memiliki parameter:


 @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class MethodToStringBenchmark { private Method method; @Setup public void setup() throws Exception { method = getClass().getMethod("toString"); } @Benchmark public int getParameterCount() { return method.getParameterCount(); } @Benchmark public int getParameterTypes() { return method.getParameterTypes().length; } } 

Di komputer saya dan dengan JDK 11 ternyata seperti ini:


 Benchmark Mode Cnt Score Error Units getParameterCount avgt 25 2,528 ± 0,085 ns/op getParameterCount:·gc.alloc.rate avgt 25 ≈ 10⁻⁴ MB/sec getParameterCount:·gc.alloc.rate.norm avgt 25 ≈ 10⁻⁷ B/op getParameterCount:·gc.count avgt 25 ≈ 0 counts getParameterTypes avgt 25 7,299 ± 0,410 ns/op getParameterTypes:·gc.alloc.rate avgt 25 1999,454 ± 89,929 MB/sec getParameterTypes:·gc.alloc.rate.norm avgt 25 16,000 ± 0,001 B/op getParameterTypes:·gc.churn.G1_Eden_Space avgt 25 2003,360 ± 91,537 MB/sec getParameterTypes:·gc.churn.G1_Eden_Space.norm avgt 25 16,030 ± 0,045 B/op getParameterTypes:·gc.churn.G1_Old_Gen avgt 25 0,004 ± 0,001 MB/sec getParameterTypes:·gc.churn.G1_Old_Gen.norm avgt 25 ≈ 10⁻⁵ B/op getParameterTypes:·gc.count avgt 25 2380,000 counts getParameterTypes:·gc.time avgt 25 1325,000 ms 

Apa yang bisa kita lakukan? Kita dapat mencari penggunaan Method.getParameterTypes().length antipattern.getParameterTypes Method.getParameterTypes().length di JDK (setidaknya di java.base ) dan ganti di tempat yang masuk akal:


java.lang.invoke.MethodHandleProxies



java.util.concurrent.ForkJoinTask



java.lang.reflect.Executable



sun.reflect.annotation.AnnotationType



Tambalan itu dikirim bersama dengan surat pengantar .


Tiba-tiba, ternyata selama beberapa tahun telah ada tugas yang serupa, dan bahkan perubahan telah disiapkan untuk itu. Komentar mencatat peningkatan kinerja yang lumayan untuk perubahan sederhana seperti itu. Namun, keduanya dan patch saya sudah dibersihkan sejauh ini dan tidak bergerak. Mengapa Mungkin karena pengembang terlalu sibuk dengan hal-hal yang lebih penting dan mereka dengan bodohnya tidak mendapatkannya.


Moral dari dongeng ini adalah ini: perubahan cerdik Anda dapat membeku hanya karena kurangnya pekerja.


Tetapi ini bukan akhir! Selama diskusi tentang rasionalitas penggantian yang dijelaskan dalam proyek lain, kawan yang lebih berpengalaman mengajukan proposal balasan: mungkin Anda tidak boleh melakukan penggantian Method.getParameterTypes().length -> Method.getParameterCount() dengan tangan Anda, tetapi percayakan ini ke kompiler? Apakah ini mungkin dan apakah akan "sah"?


Mari kita coba periksa menggunakan tes:


 @Test void arrayClone() { final Object[] objects = new Object[3]; objects[0] = "azaza"; objects[1] = 365; objects[2] = 9876L; final Object[] clone = objects.clone(); assertEquals(objects.length, clone.length); assertSame(objects[0], clone[0]); assertSame(objects[1], clone[1]); assertSame(objects[2], clone[2]); } 

yang lewat, dan yang menunjukkan bahwa jika array yang dikloning tidak meninggalkan ruang lingkup, maka itu dapat dihapus, karena akses ke elemen apa pun dari sel-selnya atau bidang length dapat diperoleh dari aslinya.


Bisakah JDK melakukan ini? Kami memeriksa:


 @State(Scope.Thread) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.NANOSECONDS) public class ArrayAllocationEliminationBenchmark { private int length = 10; @Benchmark public int baseline() { return new int[length].length; } @Benchmark public int baselineClone() { return new int[length].clone().length; } } 

Output untuk JDK 13:


 Benchmark Mode Cnt Score Error Units baseline avgt 50 6,135 ± 0,140 ns/op baseline:·gc.alloc.rate.norm avgt 50 56,000 ± 0,001 B/op clone avgt 50 18,359 ± 0,619 ns/op clone:·gc.alloc.rate.norm avgt 50 112,000 ± 0,001 B/op 

Ternyata openjdk tidak tahu cara membuang new int[length] , tidak seperti Grail , hehe:


 Benchmark Mode Cnt Score Error Units baseline avgt 25 2,470 ± 0,156 ns/op baseline:·gc.alloc.rate.norm avgt 25 0,005 ± 0,008 B/op lone avgt 25 13,086 ± 1,059 ns/op lone:·gc.alloc.rate.norm avgt 25 112,113 ± 0,115 B/op 

Ternyata Anda dapat men-tweak kompiler optimisasi openjdk sedikit sehingga dapat melakukan apa yang dapat dilakukan Grail. Karena tidak hanya semua orang dapat masuk ke iklan positif dalam kode VM dan mengajukan sesuatu yang bermakna, saya membatasi diri pada surat yang menyatakan pengamatan saya.


Ternyata, dan ada beberapa kehalusan. Vladimir Ivanov menunjukkan bahwa:


Mempertimbangkan tidak ada cara untuk menumbuhkan / mengecilkan susunan Java,
"cloned_array.length => original_array.length" transformasi benar
terlepas dari apakah varian yang dikloning lolos atau tidak.

Selain itu, transformasi sudah ada di sana:

http://hg.openjdk.java.net/jdk/jdk/file/tip/src/hotspot/share/opto/memnode.cpp#l2388

Saya belum melihat tolok ukur yang Anda sebutkan, tetapi sepertinya
akses cloned_array.length bukan alasan mengapa array kloning masih
disana

Mengenai ide-ide Anda yang lain, pengalihan akses dari turunan kloning ke
asli bermasalah (dalam kasus umum) karena kompiler harus membuktikan
tidak ada perubahan pada kedua versi dan akses yang diindeks membuatnya genap
lebih sulit. Dan titik aman menyebabkan masalah juga (untuk rematerialisasi).

Tetapi saya setuju bahwa akan lebih baik untuk meliput (setidaknya) kasus sederhana
menyalin defensif.

Yaitu, membenturkan klon sepertinya mungkin, dan tidak terlalu sulit. Tetapi dengan pertobatan


 int len = new int[arrayLength].length; 

->


 int len = arrayLength; 

kesulitan muncul:


Kami tidak menghilangkan alokasi array yang tidak diketahui panjangnya
karena mereka dapat menyebabkan Pengecualian NegatifArraySize. Dalam hal ini kita
harus bisa membuktikan bahwa panjangnya positif.

Pokoknya - Saya memiliki patch yang hampir selesai yang menggantikan array yang tidak digunakan
alokasi dengan penjaga yang tepat.

Dengan kata lain, Anda tidak bisa begitu saja mengambil dan membuang pembuatan array, karena sesuai dengan spesifikasi, eksekusi harus membuang NegativeArraySizeException dan tidak ada yang bisa kita lakukan tentang itu:


 @Test void arrayWithNwgativeSize() { int length = 0; try { int newLen = -3; length = new Object[newLen].length; //  new Object[newLen]  fail(); } catch (NegativeArraySizeException e) { assert length == 0; } } 

Mengapa Cawan bisa? Saya pikir alasannya adalah bahwa nilai bidang length pada tolok ukur di atas adalah konstan dan selalu sama dengan 10, yang memungkinkan profiler untuk menyimpulkan bahwa memeriksa nilai negatif tidak diperlukan, yang berarti dapat dihapus bersamaan dengan pembuatan array itu sendiri. Benar dalam komentar jika saya melakukan kesalahan.


Itu saja untuk hari ini :) Tambahkan contoh Anda di komentar, kami akan mengerti.

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


All Articles