Sumber
Penulis: Doug Lea bersama Brian Goetz, Paul Sandoz, Alexei Shipilev, Heinz Kabutz, Joe Bowbeer, ...
Kerangka java.util.streams
berisi operasi berbasis data pada koleksi dan sumber data lainnya. Sebagian besar metode streaming melakukan operasi yang sama pada setiap elemen. Menggunakan metode pengumpulan parallelStream()
, jika Anda memiliki banyak inti, Anda dapat mengubah data-driven menjadi data-paralel . Tetapi kapan itu layak dilakukan?
Pertimbangkan untuk menggunakan S.parallelStream().operation(F)
alih-alih S.stream().operation(F)
, dengan ketentuan bahwa operasi tersebut independen satu sama lain dan mahal secara komputasi atau diterapkan pada sejumlah besar elemen yang secara efektif dipecah (splittable) struktur data, atau keduanya. Lebih tepatnya:
F
: fungsi untuk bekerja dengan satu elemen, biasanya lambda, independen, mis. operasi pada salah satu elemen bersifat independen dan tidak mempengaruhi operasi pada elemen lain (untuk rekomendasi tentang penggunaan fungsi stateless yang tidak mengganggu, lihat dokumentasi untuk paket stream ).S
: Koleksi asli dibagi secara efektif. Selain koleksi, ada yang lain yang cocok untuk paralelisasi, streaming sumber data, misalnya, java.util.SplittableRandom
(untuk paralelisasi yang Anda dapat menggunakan metode stream.parallel()
). Tetapi sebagian besar sumber dengan I / O pada intinya dirancang terutama untuk operasi berurutan.- Total run time dalam mode berurutan melebihi batas minimum yang diijinkan. Hari ini, untuk sebagian besar platform, batasnya kira-kira sama (dalam x10) hingga 100 mikrodetik. Pengukuran yang akurat, dalam hal ini, tidak diperlukan. Untuk tujuan praktis, cukup dengan mengalikan
N
(jumlah elemen) dengan Q
(waktu operasi satu F
), dan Q
dapat diperkirakan kira-kira dengan jumlah operasi atau jumlah baris kode. Setelah itu, Anda perlu memeriksa bahwa N * Q
setidaknya kurang dari 10000
(jika Anda malu, tambahkan satu atau beberapa nol). Jadi, jika F
adalah fungsi kecil seperti x -> x + 1
, maka eksekusi paralel akan masuk akal ketika N >= 10000
. Sebaliknya, jika F
adalah perhitungan yang berbobot, mirip dengan menemukan langkah terbaik berikutnya dalam permainan catur, maka nilai Q
begitu besar sehingga N
dapat diabaikan, tetapi sampai koleksinya benar-benar terpecah.
Kerangka pemrosesan streaming tidak akan (dan tidak bisa) bersikeras pada salah satu di atas. Jika perhitungannya saling tergantung, maka eksekusi paralelnya tidak masuk akal, atau akan berbahaya sama sekali dan menyebabkan kesalahan. Kriteria lain yang berasal dari masalah teknik dan pengorbanan di atas termasuk:
- Start-up
Munculnya core tambahan dalam prosesor, dalam banyak kasus, disertai dengan penambahan mekanisme manajemen daya, yang dapat menyebabkan perlambatan dalam peluncuran kernel, kadang-kadang dengan tambahan overlay dari JVM, sistem operasi dan hypervisor. Dalam hal ini, batas di mana mode paralel masuk akal kira-kira sesuai dengan waktu yang diperlukan untuk mulai memproses subtugas dengan jumlah inti yang cukup. Setelah itu, komputasi paralel dapat lebih hemat energi daripada berurutan (tergantung pada detail prosesor dan sistem. Sebagai contoh, lihat artikel ). - Detailing (Granularity)
Jarang sekali untuk memisahkan perhitungan kecil. Kerangka kerja biasanya membagi tugas sehingga masing-masing bagian dapat bekerja pada semua inti sistem yang tersedia. Jika, setelah awal, praktis tidak ada pekerjaan untuk setiap inti, maka upaya (biasanya berurutan) untuk mengatur komputasi paralel akan sia-sia. Mengingat bahwa dalam praktiknya jumlah core berkisar dari 2 hingga 256 ambang batas, itu juga mencegah efek yang tidak diinginkan dari pembagian tugas yang berlebihan. - Dapat dibagi
Koleksi terbagi paling efisien termasuk ArrayList
dan {Concurrent}HashMap
, serta array reguler ( T[]
, yang dibagi menjadi beberapa bagian menggunakan metode java.util.Arrays
statis). Pemisah yang paling tidak efisien adalah LinkedList
, BlockingQueue
dan sebagian besar sumber dengan berbasis I / O. Sisanya berada di suatu tempat di tengah (struktur data yang mendukung akses acak dan / atau pencarian efisien biasanya dibagi secara efisien). Jika pemisahan data lebih lama dari pemrosesan, maka upaya itu sia-sia. Jika Q
cukup besar, maka Anda bisa mendapatkan peningkatan karena paralelisasi bahkan untuk LinkedList
, tetapi ini adalah kasus yang agak jarang. Selain itu, beberapa sumber tidak dapat dipecah menjadi satu elemen tunggal, dan dengan demikian, mungkin ada batasan pada tingkat penguraian masalah.
Mendapatkan karakteristik yang tepat dari efek-efek ini bisa sulit (walaupun, jika Anda mencoba, itu dapat dilakukan dengan menggunakan alat seperti JMH ). Tetapi efek kumulatifnya cukup mudah dilihat. Untuk merasakannya sendiri - lakukan percobaan. Misalnya, pada mesin uji 32-core, ketika Anda menjalankan fungsi kecil, seperti max()
atau sum()
, di atas ArrayList
titik impas adalah sekitar 10.000. Untuk elemen lainnya, akselerasi hingga 20 kali dicatat. Jam buka untuk koleksi dengan kurang dari 10.000 item tidak kurang dari untuk 10.000, dan karenanya lebih lambat dari pemrosesan berurutan. Hasil terburuk terjadi dengan kurang dari 100 elemen - dalam hal ini, utas yang terlibat berhenti tanpa melakukan sesuatu yang bermanfaat, karena perhitungan selesai sebelum mereka mulai. Di sisi lain, ketika operasi pada elemen memakan waktu, ketika menggunakan koleksi yang efisien dan sepenuhnya dapat dipecah, seperti ArrayList
, manfaatnya segera terlihat.
Untuk memparafrasekan semua hal di atas, penggunaan parallel()
dalam kasus jumlah komputasi yang tidak masuk akal dapat menghabiskan biaya sekitar 100
mikrodetik, dan penggunaan sebaliknya akan menghemat setidaknya saat ini sendiri (atau mungkin berjam-jam untuk tugas yang sangat besar). Biaya dan manfaat spesifik akan bervariasi dari waktu ke waktu untuk platform yang berbeda, dan juga, tergantung pada konteksnya. Misalnya, menjalankan perhitungan kecil secara paralel dalam siklus berurutan meningkatkan efek pasang surut (kinerja microtest di mana hal ini terjadi mungkin tidak mencerminkan situasi sebenarnya).
Tanya Jawab
- Mengapa JVM tidak bisa mengerti kapan harus menjalankan operasi secara paralel?
Dia mungkin mencoba, tetapi terlalu sering keputusannya salah. Pencarian untuk paralelisme multi-core yang sepenuhnya otomatis tidak menghasilkan solusi universal selama tiga puluh tahun terakhir, dan oleh karena itu, kerangka kerja ini menggunakan pendekatan yang lebih andal, yang mengharuskan pengguna hanya untuk memilih antara ya atau tidak . Pilihan ini didasarkan pada masalah teknik yang terus-menerus ditemui dalam pemrograman berurutan, yang tidak mungkin hilang sama sekali. Misalnya, Anda mungkin mengalami pelambatan seratus kali lipat ketika mencari nilai maksimum dalam koleksi yang mengandung elemen tunggal dibandingkan dengan menggunakan nilai ini secara langsung (tanpa koleksi). Terkadang JVM dapat mengoptimalkan kasus seperti itu untuk Anda. Tapi ini jarang terjadi dalam kasus berurutan, dan tidak pernah dalam kasus mode paralel. Di sisi lain, kita dapat berharap bahwa, ketika mereka berkembang, alat akan membantu pengguna membuat keputusan yang lebih baik.
- Bagaimana jika untuk membuat keputusan yang baik saya tidak memiliki pengetahuan yang cukup tentang parameter (
F
, N
, Q
, S
)?
Ini juga mirip dengan masalah yang dihadapi dalam pemrograman berurutan. Misalnya, metode S.contains(x)
dari kelas Collection
biasanya berjalan cepat jika S
adalah HashSet
, lambat jika LinkedList
, dan rata-rata dalam kasus lain. Biasanya, untuk pembuat komponen yang menggunakan koleksi, jalan keluar terbaik dari situasi ini adalah merangkumnya dan hanya mempublikasikan operasi spesifik di dalamnya. Maka pengguna akan terisolasi dari kebutuhan untuk memilih. Hal yang sama berlaku untuk operasi paralel. Misalnya, komponen dengan pengumpulan harga internal dapat menentukan metode yang memeriksa ukurannya hingga batasnya, yang akan masuk akal sampai komputasi bitwise terlalu mahal. Contoh:
public long getMaxPrice() { return priceStream().max(); } private Stream priceStream() { return (prices.size() < MIN_PAR) ? prices.stream() : prices.parallelStream(); }
Gagasan ini dapat diperluas ke pertimbangan lain tentang kapan dan bagaimana menggunakan konkurensi.
- Bagaimana jika fungsi saya mungkin melakukan operasi I / O atau tersinkronisasi?
Pada satu ekstrem adalah fungsi yang tidak memenuhi kriteria independensi, termasuk operasi I / O berurutan, akses ke sumber daya penguncian yang disinkronkan, dan kasus di mana kesalahan dalam satu sub-tugas paralel yang melakukan I / O memengaruhi yang lain. Paralelasinya tidak masuk akal. Di sisi lain, ada perhitungan yang kadang-kadang melakukan sinkronisasi I / O atau jarang diblokir (misalnya, sebagian besar kasus logging, dan penggunaan koleksi kompetitif seperti ConcurrentHashMap
). Mereka tidak berbahaya. Apa yang ada di antara mereka membutuhkan penelitian lebih lanjut. Jika setiap subtugas dapat diblokir untuk waktu yang cukup lama menunggu I / O atau akses, sumber daya CPU akan menganggur tanpa kemungkinan penggunaannya oleh program atau JVM. Dari ini buruk untuk semua orang. Dalam kasus ini, pemrosesan streaming paralel tidak selalu merupakan pilihan yang tepat. Tetapi ada alternatif yang baik - misalnya, asynchronous I / O dan pendekatan CompletableFuture
.
- Bagaimana jika sumber saya didasarkan pada I / O?
Saat ini, menggunakan generator JDK Stream
/ I / O (misalnya, BufferedReader.lines()
), mereka terutama diadaptasi untuk digunakan dalam mode berurutan, memproses elemen satu per satu saat tersedia. Dukungan untuk pemrosesan massal berkinerja tinggi dari buffer I / O dimungkinkan, tetapi, pada saat ini, ini memerlukan pengembangan generator khusus Stream
s, Spliterator
dan Collector
s. Dukungan untuk beberapa kasus umum dapat ditambahkan dalam rilis JDK mendatang.
- Bagaimana jika program saya berjalan di komputer yang sibuk dan semua kernel sibuk?
Mesin biasanya memiliki jumlah inti yang tetap, dan tidak dapat secara ajaib membuat yang baru ketika melakukan operasi paralel. Namun, selama kriteria untuk memilih mode paralel jelas digunakan , tidak ada yang perlu diragukan. Tugas paralel Anda akan bersaing untuk CPU dengan orang lain dan Anda akan melihat lebih sedikit akselerasi. Dalam kebanyakan kasus, ini masih lebih efektif daripada alternatif lain. Mekanisme yang mendasarinya dirancang sedemikian rupa sehingga jika tidak ada kernel yang tersedia, Anda hanya akan melihat sedikit perlambatan dibandingkan versi sekuensial, kecuali ketika sistem kelebihan beban sehingga menghabiskan semua waktu untuk mengubah konteks alih-alih melakukan pekerjaan nyata, atau dikonfigurasi dengan harapan bahwa semua pemrosesan dilakukan secara berurutan. Jika Anda memiliki sistem seperti itu, maka mungkin administrator telah menonaktifkan penggunaan multithreading / nuklir dalam pengaturan JVM. Dan jika Anda adalah administrator sistem, masuk akal untuk melakukan ini.
- Apakah semua operasi diparalelkan saat menggunakan mode paralel?
Ya Setidaknya sampai batas tertentu. Tetapi perlu mempertimbangkan bahwa kerangka-aliran memperhitungkan keterbatasan sumber dan metode saat memilih cara melakukan ini. Secara umum, semakin sedikit pembatasan, semakin besar potensi paralelisme. Di sisi lain, tidak ada jaminan bahwa kerangka kerja akan mengidentifikasi dan menerapkan semua peluang yang tersedia untuk konkurensi. Dalam beberapa kasus, jika Anda memiliki waktu dan kompetensi, solusi Anda sendiri dapat memanfaatkan kemungkinan konkurensi dengan lebih baik.
- Akselerasi apa yang akan saya dapatkan dari concurrency?
Jika Anda mengikuti tips ini, maka, biasanya, cukup masuk akal. Prediktabilitas bukanlah titik kuat dari perangkat keras dan sistem modern, dan oleh karena itu tidak ada jawaban universal. Lokalitas cache, karakteristik GC, kompilasi JIT, konflik akses memori, lokasi data, kebijakan penjadwalan OS, dan keberadaan hypervisor adalah beberapa faktor yang memiliki dampak signifikan. Kinerja mode sekuensial juga tunduk pada pengaruhnya, yang, ketika menggunakan paralelisme, sering diperkuat: masalah yang menyebabkan perbedaan 10 persen dalam kasus eksekusi sekuensial dapat menyebabkan perbedaan 10 kali lipat dalam pemrosesan paralel.
Kerangka aliran mencakup beberapa fitur yang membantu meningkatkan kemungkinan akselerasi. Misalnya, menggunakan spesialisasi untuk primitif, seperti IntStream
, biasanya memiliki efek yang lebih besar untuk mode paralel daripada untuk mode berurutan. Alasannya adalah bahwa dalam kasus ini, tidak hanya konsumsi sumber daya (dan memori) menurun, tetapi lokasi cache juga meningkat. Menggunakan ConcurrentHashMap
alih-alih HashMap
, dalam kasus operasi paralel dari operasi collect
, mengurangi biaya internal. Kiat dan trik baru akan muncul sebagai pengalaman yang diperoleh dengan kerangka kerja.
- Semua ini terlalu menakutkan! Tidak bisakah kita membuat aturan untuk menggunakan properti JVM untuk mematikan konkurensi?
Kami tidak ingin memberi tahu Anda apa yang harus dilakukan. Munculnya cara baru bagi programmer untuk melakukan sesuatu yang salah bisa menakutkan. Kesalahan dalam kode, arsitektur, dan evaluasi pasti akan terjadi. Beberapa dekade yang lalu, beberapa orang meramalkan bahwa konkurensi pada tingkat aplikasi akan menyebabkan bencana besar. Tapi itu tidak pernah menjadi kenyataan.