Haruskah saya selalu menggunakan aliran paralel jika memungkinkan?

515

Dengan Java 8 dan lambdas, mudah untuk mengulangi koleksi sebagai stream, dan juga mudah menggunakan stream paralel. Dua contoh dari dokumen , yang kedua menggunakan parallelStream:

myShapesCollection.stream()
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

myShapesCollection.parallelStream() // <-- This one uses parallel
    .filter(e -> e.getColor() == Color.RED)
    .forEach(e -> System.out.println(e.getName()));

Selama saya tidak peduli dengan pesanan, apakah akan selalu bermanfaat untuk menggunakan paralel? Orang akan berpikir lebih cepat membagi pekerjaan pada lebih banyak core.

Apakah ada pertimbangan lain? Kapan aliran paralel digunakan dan kapan non-paralel digunakan?

(Pertanyaan ini diminta untuk memicu diskusi tentang bagaimana dan kapan menggunakan aliran paralel, bukan karena saya pikir selalu menggunakannya adalah ide yang bagus.)

Matsemann
sumber

Jawaban:

736

Aliran paralel memiliki overhead yang jauh lebih tinggi dibandingkan dengan aliran berurutan. Mengkoordinasikan utas membutuhkan banyak waktu. Saya akan menggunakan aliran berurutan secara default dan hanya mempertimbangkan yang sejajar jika

  • Saya memiliki sejumlah besar item untuk diproses (atau pemrosesan setiap item membutuhkan waktu dan dapat diparalelkan)

  • Saya memiliki masalah kinerja di tempat pertama

  • Saya belum menjalankan proses dalam lingkungan multi-utas (misalnya: dalam wadah web, jika saya sudah memiliki banyak permintaan untuk diproses secara paralel, menambahkan lapisan paralelisme tambahan di dalam setiap permintaan dapat memiliki lebih banyak efek negatif daripada efek positif )

Dalam contoh Anda, kinerja tetap akan didorong oleh akses yang disinkronkan ke System.out.println() , dan membuat proses ini paralel tidak akan berpengaruh, atau bahkan yang negatif.

Selain itu, ingatlah bahwa aliran paralel tidak secara ajaib menyelesaikan semua masalah sinkronisasi. Jika sumber daya bersama digunakan oleh predikat dan fungsi yang digunakan dalam proses, Anda harus memastikan bahwa semuanya aman. Secara khusus, efek samping adalah hal-hal yang harus Anda khawatirkan jika Anda lakukan secara paralel.

Bagaimanapun, ukurlah, jangan menebak! Hanya pengukuran yang akan memberi tahu Anda apakah paralelisme itu sepadan atau tidak.

JB Nizet
sumber
18
Jawaban yang bagus. Saya akan menambahkan bahwa jika Anda memiliki sejumlah besar item untuk diproses, itu hanya meningkatkan masalah koordinasi utas; itu hanya ketika memproses setiap item membutuhkan waktu dan diparalelkan sehingga paralelisasi mungkin berguna.
Warren Dew
16
@ WarrenDew saya tidak setuju. Sistem Fork / Join hanya akan membagi item N menjadi, misalnya, 4 bagian, dan memproses 4 bagian ini secara berurutan. 4 hasil kemudian akan dikurangi. Jika masif benar-benar masif, bahkan untuk pemrosesan satuan cepat, paralelisasi bisa efektif. Tetapi seperti biasa, Anda harus mengukur.
JB Nizet
saya memiliki kumpulan objek yang mengimplementasikan Runnableyang saya panggil start()untuk menggunakannya Threads, apakah boleh mengubah itu menggunakan java 8 stream secara .forEach()paralel? Maka saya bisa menghapus kode utas dari kelas. Tetapi apakah ada kerugian?
ycomp
1
@ JBNizet Jika 4 bagian diproses secara berurutan, maka tidak ada perbedaan apakah itu proses paralel atau berurutan? Mohon klarifikasi
Harshana
3
@ Harshana dia jelas berarti bahwa unsur-unsur dari masing-masing 4 bagian akan diproses secara berurutan. Namun, bagian itu sendiri dapat diproses secara bersamaan. Dengan kata lain, jika Anda memiliki beberapa core CPU yang tersedia, masing-masing bagian dapat berjalan pada intinya sendiri secara terpisah dari bagian lainnya, sambil memproses elemen-elemennya sendiri secara berurutan. (CATATAN: Saya tidak tahu, jika beginilah paralel aliran Java bekerja, saya hanya mencoba untuk menjelaskan apa yang dimaksud JBNizet.)
besok
258

Stream API dirancang untuk membuatnya mudah untuk menulis perhitungan dengan cara yang disarikan dari bagaimana mereka akan dieksekusi, membuat beralih antara sekuensial dan paralel menjadi mudah.

Namun, hanya karena mudah, tidak berarti selalu merupakan ide yang bagus, dan pada kenyataannya, itu adalah ide yang buruk untuk drop.parallel() seluruh tempat hanya karena Anda bisa.

Pertama, perhatikan bahwa paralelisme tidak menawarkan manfaat selain kemungkinan eksekusi lebih cepat ketika lebih banyak core tersedia. Eksekusi paralel akan selalu melibatkan lebih banyak pekerjaan daripada yang berurutan, karena selain menyelesaikan masalah, ia juga harus melakukan pengiriman dan koordinasi sub-tugas. Harapannya adalah Anda akan bisa mendapatkan jawaban lebih cepat dengan memecah pekerjaan di beberapa prosesor; apakah ini benar-benar terjadi tergantung pada banyak hal, termasuk ukuran kumpulan data Anda, berapa banyak perhitungan yang Anda lakukan pada setiap elemen, sifat perhitungan (khususnya, apakah pemrosesan satu elemen berinteraksi dengan pemrosesan yang lain?) , jumlah prosesor yang tersedia, dan jumlah tugas lain yang bersaing untuk prosesor tersebut.

Lebih lanjut, perhatikan bahwa paralelisme juga sering memperlihatkan nondeterminisme dalam perhitungan yang sering disembunyikan oleh implementasi berurutan; kadang-kadang ini tidak masalah, atau dapat dikurangi dengan membatasi operasi yang terlibat (yaitu, operator reduksi harus stateless dan asosiatif.)

Pada kenyataannya, terkadang paralelisme akan mempercepat perhitungan Anda, terkadang tidak, dan terkadang bahkan memperlambatnya. Yang terbaik adalah mengembangkan terlebih dahulu menggunakan eksekusi berurutan dan kemudian menerapkan paralelisme di mana

(A) Anda tahu bahwa sebenarnya ada manfaat untuk peningkatan kinerja dan

(B) bahwa itu benar-benar akan memberikan peningkatan kinerja.

(A) adalah masalah bisnis, bukan masalah teknis. Jika Anda seorang ahli kinerja, Anda biasanya dapat melihat kode dan menentukan (B), tetapi jalur pintar untuk mengukur. (Dan, jangan repot-repot sampai Anda yakin (A); jika kodenya cukup cepat, lebih baik untuk menerapkan siklus otak Anda di tempat lain.)

Model kinerja paling sederhana untuk paralelisme adalah model "NQ", di mana N adalah jumlah elemen, dan Q adalah perhitungan per elemen. Secara umum, Anda memerlukan produk NQ untuk melebihi ambang batas sebelum Anda mulai mendapatkan manfaat kinerja. Untuk masalah Q rendah seperti "tambahkan angka dari 1 ke N", Anda biasanya akan melihat titik impas antara N = 1000 dan N = 10.000. Dengan masalah Q yang lebih tinggi, Anda akan melihat breakevens di ambang yang lebih rendah.

Tetapi kenyataannya cukup rumit. Jadi sampai Anda mencapai keahlian, pertama-tama kenali kapan pemrosesan sekuensial benar-benar merugikan Anda, dan kemudian ukur apakah paralelisme akan membantu.

Brian Goetz
sumber
18
Posting ini memberikan rincian lebih lanjut tentang model NQ: gee.cs.oswosatedu
Pino
4
@specializt: mengalihkan aliran dari sekuensial ke paralel tidak mengubah algoritma (dalam kebanyakan kasus). Determinisme yang disebutkan di sini adalah mengenai properti Anda (sewenang-wenang) operator mungkin bergantung pada (pelaksanaan Streaming tidak bisa tahu itu), tapi tentu saja tidak harus bergantung pada. Itulah yang coba dijawab oleh bagian dari jawaban ini. Jika Anda peduli dengan aturan, Anda dapat memiliki hasil deterministik, seperti yang Anda katakan, (jika tidak, aliran paralel cukup tidak berguna), tetapi ada juga kemungkinan untuk secara sengaja mengizinkan non-determinisme, seperti ketika menggunakan findAnyalih-alih findFirst...
Holger
4
"Pertama, perhatikan bahwa paralelisme tidak menawarkan manfaat selain kemungkinan eksekusi lebih cepat ketika lebih banyak core tersedia" - atau jika Anda menerapkan tindakan yang melibatkan IO (misalnya myListOfURLs.stream().map((url) -> downloadPage(url))...).
Jules
6
@Pacerier Itu teori yang bagus, tapi naif sekali (lihat sejarah 30 tahun upaya untuk membangun kompilator penjajaran otomatis sebagai permulaan). Karena tidak praktis untuk menebak dengan tepat waktu untuk tidak mengganggu pengguna ketika kami pasti salah, hal yang bertanggung jawab untuk dilakukan adalah membiarkan pengguna untuk mengatakan apa yang mereka inginkan. Untuk sebagian besar situasi, default (berurutan) benar, dan lebih dapat diprediksi.
Brian Goetz
2
@ Jules: Jangan pernah gunakan stream paralel untuk IO. Mereka hanya dimaksudkan untuk operasi intensif CPU. Aliran paralel digunakan ForkJoinPool.commonPool()dan Anda tidak ingin memblokir tugas untuk pergi ke sana.
R2C2
68

Aku melihat salah satu presentasi dari Brian Goetz (Bahasa Jawa Arsitek & spesifikasi memimpin untuk Ekspresi Lambda) . Dia menjelaskan secara rinci 4 poin berikut untuk dipertimbangkan sebelum pergi untuk paralelisasi:

Biaya pemisahan / penguraian
- Kadang-kadang pemisahan lebih mahal daripada hanya melakukan pekerjaan!
Pengiriman
tugas / biaya manajemen - Dapat melakukan banyak pekerjaan dalam waktu yang diperlukan untuk menyerahkan pekerjaan ke utas lainnya.
Biaya kombinasi hasil
- Kadang kombinasi melibatkan penyalinan banyak data. Misalnya, menambahkan angka itu murah sedangkan menggabungkan set itu mahal.
Lokalitas
- Gajah di dalam ruangan. Ini adalah poin penting yang mungkin dilewatkan semua orang. Anda harus mempertimbangkan kesalahan cache, jika CPU menunggu data karena cache hilang, maka Anda tidak akan mendapatkan apa pun dengan paralelisasi. Itu sebabnya sumber berbasis array memparalelkan yang terbaik sebagai indeks berikutnya (dekat indeks saat ini) di-cache dan ada sedikit kemungkinan bahwa CPU akan mengalami cache miss.

Dia juga menyebutkan formula yang relatif sederhana untuk menentukan peluang percepatan paralel.

Model NQ :

N x Q > 10000

di mana,
N = jumlah item data
Q = jumlah pekerjaan per item

Ram Patra
sumber
13

JB memukul kepala. Satu-satunya hal yang dapat saya tambahkan adalah bahwa Java 8 tidak melakukan pemrosesan paralel murni, itu tidak parsial . Ya saya menulis artikel dan saya sudah melakukan F / J selama tiga puluh tahun jadi saya mengerti masalah ini.

edharned
sumber
10
Streaming tidak dapat diubah karena stream melakukan iterasi internal, bukan eksternal. Lagipula itulah alasan utama untuk streaming. Jika Anda memiliki masalah dengan pekerjaan akademik maka pemrograman fungsional mungkin bukan untuk Anda. Pemrograman fungsional === matematika === akademik. Dan tidak, J8-FJ tidak rusak, hanya saja sebagian besar orang tidak membaca manual f ******. Java docs mengatakan dengan sangat jelas bahwa itu bukan kerangka kerja eksekusi paralel. Itulah alasan utama semua alat pembagi. Ya itu akademis, ya itu bekerja jika Anda tahu cara menggunakannya. Ya itu seharusnya lebih mudah menggunakan eksekutor khusus
Kr0e
1
Stream memang memiliki metode iterator (), sehingga Anda dapat menggunakannya secara eksternal jika Anda mau. Pemahaman saya adalah bahwa mereka tidak mengimplementasikan Iterable karena Anda hanya dapat menggunakan iterator sekali dan tidak ada yang bisa memutuskan apakah itu OK.
Trejkaz
14
sejujurnya: seluruh makalah Anda berbunyi seperti kata-kata kasar yang rumit - dan itu cukup banyak meniadakan kredibilitasnya ... saya akan merekomendasikan untuk melakukannya kembali dengan nada yang jauh kurang agresif jika tidak banyak orang yang benar-benar akan repot-repot membacanya sepenuhnya ... saya hanya sayan
spesialis
Beberapa pertanyaan tentang artikel Anda ... pertama-tama, mengapa Anda tampaknya menyamakan struktur pohon seimbang dengan grafik asiklik langsung? Ya, pohon seimbang adalah DAG, tetapi begitu pula daftar yang ditautkan dan hampir setiap struktur data berorientasi objek selain array. Juga, ketika Anda mengatakan dekomposisi rekursif hanya bekerja pada struktur pohon seimbang dan karena itu tidak relevan secara komersial, bagaimana Anda membenarkan pernyataan itu? Tampaknya bagi saya (harus diakui tanpa benar-benar memeriksa masalah secara mendalam) bahwa itu harus bekerja dengan baik pada struktur data berbasis array, misalnya ArrayList/ HashMap.
Jules
1
Utas ini dari 2013, banyak yang telah berubah sejak saat itu. Bagian ini untuk komentar bukan jawaban terperinci.
edharned
3

Jawaban lain telah mencakup pembuatan profil untuk menghindari optimasi prematur dan biaya overhead dalam pemrosesan paralel. Jawaban ini menjelaskan pilihan ideal struktur data untuk streaming paralel.

Sebagai aturan, kinerja keuntungan dari paralelisme yang terbaik di sungai lebih ArrayList, HashMap, HashSet, dan ConcurrentHashMapcontoh; array; intrentang; dan longrentang. Apa kesamaan struktur data ini adalah bahwa semuanya dapat secara akurat dan murah dipecah menjadi subrange dari ukuran yang diinginkan, yang membuatnya mudah untuk membagi pekerjaan di antara thread paralel. Abstraksi yang digunakan oleh perpustakaan stream untuk melakukan tugas ini adalah spliterator, yang dikembalikan oleh spliteratormetode on StreamdanIterable .

Faktor penting lain yang dimiliki oleh semua struktur data ini adalah bahwa mereka menyediakan lokalitas referensi yang sangat baik ketika diproses secara berurutan: referensi elemen berurutan disimpan bersama dalam memori. Objek yang dirujuk oleh referensi tersebut mungkin tidak berdekatan satu sama lain dalam memori, yang mengurangi referensi lokalitas. Referensi lokalitas ternyata sangat penting untuk memparalelkan operasi massal: tanpanya, thread menghabiskan banyak waktu mereka, menunggu data ditransfer dari memori ke cache prosesor. Struktur data dengan lokalitas referensi terbaik adalah array primitif karena data itu sendiri disimpan secara berdampingan dalam memori.

Sumber: Item # 48 Gunakan Perhatian Saat Membuat Streaming Paralel, Java 3e yang Efektif oleh Joshua Bloch

ruhong
sumber
2

Jangan pernah mensejajarkan aliran tanpa batas dengan batas. Inilah yang terjadi:

    public static void main(String[] args) {
        // let's count to 1 in parallel
        System.out.println(
            IntStream.iterate(0, i -> i + 1)
                .parallel()
                .skip(1)
                .findFirst()
                .getAsInt());
    }

Hasil

    Exception in thread "main" java.lang.OutOfMemoryError
        at ...
        at java.base/java.util.stream.IntPipeline.findFirst(IntPipeline.java:528)
        at InfiniteTest.main(InfiniteTest.java:24)
    Caused by: java.lang.OutOfMemoryError: Java heap space
        at java.base/java.util.stream.SpinedBuffer$OfInt.newArray(SpinedBuffer.java:750)
        at ...

Sama jika Anda gunakan .limit(...)

Penjelasan di sini: Java 8, menggunakan .parallel dalam aliran menyebabkan kesalahan OOM

Demikian pula, jangan gunakan paralel jika aliran diurutkan dan memiliki lebih banyak elemen daripada yang ingin Anda proses, misalnya

public static void main(String[] args) {
    // let's count to 1 in parallel
    System.out.println(
            IntStream.range(1, 1000_000_000)
                    .parallel()
                    .skip(100)
                    .findFirst()
                    .getAsInt());
}

Ini mungkin berjalan lebih lama karena utas paralel dapat bekerja pada banyak rentang bilangan alih-alih yang penting 0-100, menyebabkan ini membutuhkan waktu yang sangat lama.

tkruse
sumber