ByteBuffer.allocate () vs. ByteBuffer.allocateDirect ()

144

Ke allocate()atau ke allocateDirect(), itulah pertanyaannya.

Selama beberapa tahun sekarang saya hanya terjebak pada pemikiran bahwa karena DirectByteBuffers adalah pemetaan memori langsung pada tingkat OS, itu akan melakukan lebih cepat dengan panggilan get / put daripada HeapByteBuffers. Saya tidak pernah benar-benar tertarik untuk mencari tahu detail yang tepat mengenai situasi sampai sekarang. Saya ingin tahu yang mana dari dua jenis ByteBuffers yang lebih cepat dan pada kondisi apa.

ROMANIA_engineer
sumber
Untuk memberikan jawaban spesifik, Anda perlu mengatakan secara spesifik apa yang Anda lakukan dengan mereka. Jika satu selalu lebih cepat dari yang lain, mengapa ada dua varian. Mungkin Anda dapat memperluas alasan mengapa Anda sekarang "benar-benar tertarik untuk mencari tahu perincian yang tepat" BTW: Sudahkah Anda membaca kode, esp untuk DirectByteBuffer?
Peter Lawrey
Mereka akan digunakan untuk membaca dari dan menulis ke SocketChannels yang dikonfigurasi untuk non-blocking. Jadi mengenai apa yang dikatakan @bmargulies, DirectByteBuffers akan bekerja lebih cepat untuk saluran.
@Gnarly Setidaknya versi jawaban saya saat ini mengatakan bahwa saluran diharapkan mendapat manfaat.
bmargulies

Jawaban:

150

Ron Hitches dalam bukunya yang luar biasa Java NIO tampaknya menawarkan apa yang saya pikir bisa menjadi jawaban yang baik untuk pertanyaan Anda:

Sistem operasi melakukan operasi I / O pada area memori. Area memori ini, sejauh menyangkut sistem operasi, adalah urutan byte yang berdekatan. Maka tidak mengherankan bahwa hanya buffer byte yang memenuhi syarat untuk berpartisipasi dalam operasi I / O. Ingat juga bahwa sistem operasi akan secara langsung mengakses ruang alamat proses, dalam hal ini proses JVM, untuk mentransfer data. Ini berarti bahwa area memori yang menjadi target operasi I / O harus bersebelahan dengan urutan byte. Dalam JVM, array byte mungkin tidak disimpan secara bersamaan di memori, atau Pengumpul Sampah dapat memindahkannya kapan saja. Array adalah objek di Jawa, dan cara data disimpan di dalam objek itu bisa bervariasi dari satu implementasi JVM ke yang lain.

Untuk alasan ini, gagasan buffer langsung diperkenalkan. Buffer langsung dimaksudkan untuk interaksi dengan saluran dan rutin I / O asli. Mereka melakukan upaya terbaik untuk menyimpan elemen byte di area memori yang dapat digunakan saluran untuk akses langsung, atau mentah, dengan menggunakan kode asli untuk memberi tahu sistem operasi untuk mengalirkan atau mengisi area memori secara langsung.

Buffer byte langsung biasanya merupakan pilihan terbaik untuk operasi I / O. Secara desain, mereka mendukung mekanisme I / O paling efisien yang tersedia untuk JVM. Buffer byte tidak langsung dapat diteruskan ke saluran, tetapi hal itu dapat dikenakan penalti kinerja. Biasanya tidak mungkin buffer tidak langsung menjadi target operasi I / O asli. Jika Anda meneruskan objek ByteBuffer tidak langsung ke saluran untuk menulis, saluran tersebut secara implisit dapat melakukan hal berikut pada setiap panggilan:

  1. Buat objek ByteBuffer langsung sementara.
  2. Salin konten buffer nondirect ke buffer sementara.
  3. Lakukan operasi I / O tingkat rendah menggunakan buffer sementara.
  4. Objek buffer sementara keluar dari ruang lingkup dan akhirnya dikumpulkan sampah.

Ini berpotensi menghasilkan penyalinan buffer dan objek churn pada setiap I / O, yang persis seperti hal-hal yang ingin kita hindari. Namun, tergantung pada implementasinya, semuanya mungkin tidak seburuk ini. Runtime kemungkinan akan melakukan cache dan menggunakan kembali buffer langsung atau melakukan trik pintar lainnya untuk meningkatkan throughput. Jika Anda hanya membuat buffer untuk sekali pakai, perbedaannya tidak signifikan. Di sisi lain, jika Anda akan menggunakan buffer berulang kali dalam skenario kinerja tinggi, Anda lebih baik mengalokasikan buffer langsung dan menggunakannya kembali.

Buffer langsung optimal untuk I / O, tetapi mereka mungkin lebih mahal untuk dibuat daripada buffer byte tidak langsung. Memori yang digunakan oleh buffer langsung dialokasikan dengan memanggil kode asli, sistem operasi spesifik, melewati tumpukan JVM standar. Menyiapkan dan menghancurkan buffer langsung bisa jauh lebih mahal daripada buffer-residen, tergantung pada sistem operasi host dan implementasi JVM. Area penyimpanan memori buffer langsung tidak terkena pengumpulan sampah karena mereka berada di luar tumpukan JVM standar.

Pengorbanan kinerja menggunakan buffer langsung versus tidak langsung dapat sangat bervariasi menurut JVM, sistem operasi, dan desain kode. Dengan mengalokasikan memori di luar heap, Anda dapat membuat aplikasi Anda ke kekuatan tambahan yang JVM tidak sadari. Saat memainkan komponen bergerak tambahan, pastikan Anda mencapai efek yang diinginkan. Saya merekomendasikan pepatah perangkat lunak lama: pertama membuatnya bekerja, kemudian membuatnya cepat. Jangan terlalu khawatir tentang pengoptimalan di muka; berkonsentrasi pertama pada kebenaran. Implementasi JVM mungkin dapat melakukan caching buffer atau optimasi lain yang akan memberi Anda kinerja yang Anda butuhkan tanpa banyak upaya yang tidak perlu di pihak Anda.

Edwin Dalorzo
sumber
9
Saya tidak suka kutipan itu karena mengandung terlalu banyak tebakan. Juga, JVM tentu saja tidak perlu mengalokasikan ByteBuffer langsung ketika melakukan IO untuk ByteBuffer non-langsung: itu cukup untuk malloc urutan byte pada heap, lakukan IO, salin dari byte ke ByteBuffer dan lepaskan byte. Area-area itu bahkan bisa di-cache. Tetapi sama sekali tidak perlu mengalokasikan objek Java untuk ini. Jawaban nyata hanya akan diperoleh dari pengukuran. Terakhir kali saya melakukan pengukuran tidak ada perbedaan yang signifikan. Saya harus mengulang tes untuk datang dengan semua detail spesifik.
Robert Klemme
4
Masih dipertanyakan apakah buku yang menggambarkan NIO (dan operasi asli) dapat memiliki kepastian di dalamnya. Bagaimanapun, JVM dan sistem operasi yang berbeda mengelola berbagai hal secara berbeda, sehingga penulis tidak dapat disalahkan karena tidak dapat menjamin perilaku tertentu.
Martin Tuskevicius
@RobertKlemme, +1, kita semua membenci dugaan itu, Namun, mungkin tidak mungkin untuk mengukur kinerja untuk semua OS utama, karena ada terlalu banyak OS utama. Posting lain mencoba itu, tetapi kita dapat melihat banyak masalah dengan tolok ukur itu, dimulai dengan "hasilnya berfluktuasi secara luas tergantung pada OS". Juga, bagaimana jika ada domba hitam yang melakukan hal-hal mengerikan seperti menyalin buffer pada setiap I / O? Kemudian karena domba itu, kita mungkin dipaksa untuk mencegah penulisan kode yang seharusnya kita gunakan, hanya untuk menghindari skenario terburuk ini.
Pacerier
@RobertKlemme saya setuju. Terlalu banyak dugaan di sini. JVM sepertinya tidak mungkin untuk mengalokasikan byte array jarang, misalnya.
Marquis of Lorne
@ Edwin Dalorzo: Mengapa kita membutuhkan byte byte seperti itu di dunia nyata? Apakah mereka diciptakan sebagai peretasan untuk membagi memori di antara proses? Katakan misalnya JVM berjalan pada suatu proses dan itu akan menjadi proses lain yang berjalan pada jaringan atau lapisan data link - yang bertanggung jawab untuk mengirimkan data - apakah buffer byte ini dialokasikan untuk berbagi memori antara proses-proses ini? Tolong koreksi saya jika saya salah ..
Tom Taylor
25

Tidak ada alasan untuk mengharapkan buffer langsung lebih cepat untuk akses di dalam jvm. Keuntungan mereka datang ketika Anda meneruskannya ke kode asli - seperti, kode di balik semua saluran.

bmargulies
sumber
Memang. Seperti, ketika perlu melakukan IO di Scala / Java dan memanggil embedded Python / libs asli dengan data memori yang besar untuk pemrosesan algoritmik atau memasukkan data langsung ke GPU di Tensorflow.
SemanticBeeng
21

karena DirectByteBuffers adalah pemetaan memori langsung di tingkat OS

Mereka bukan. Mereka hanya memori proses aplikasi normal, tetapi tidak mengalami relokasi selama Java GC yang menyederhanakan banyak hal di dalam lapisan JNI. Apa yang Anda uraikan berlaku untuk MappedByteBuffer.

bahwa itu akan melakukan lebih cepat dengan panggilan get / put

Kesimpulannya tidak mengikuti dari premis; premisnya salah; dan kesimpulannya juga salah. Mereka lebih cepat begitu Anda masuk ke dalam lapisan JNI, dan jika Anda membaca dan menulis dari yang sama DirectByteBuffermereka jauh lebih cepat, karena data tidak pernah harus melewati batas JNI sama sekali.

Marquis dari Lorne
sumber
7
Ini adalah poin yang baik dan penting: di jalur IO Anda harus melintasi perbatasan Jawa - JNI di beberapa titik. Buffer byte langsung dan tidak langsung hanya memindahkan perbatasan: dengan buffer langsung semua operasi put dari tanah Jawa harus dilintasi, sedangkan dengan buffer non-langsung semua operasi IO harus dilintasi. Apa yang lebih cepat tergantung pada aplikasi.
Robert Klemme
@RobertKlemme Ringkasan Anda salah. Dengan semua buffer, setiap data yang datang ke dan dari Jawa harus melewati batas JNI. Maksud dari buffer langsung adalah bahwa jika Anda hanya menyalin data dari satu saluran ke yang lain, misalnya mengunggah file, Anda tidak harus memasukkannya ke Jawa sama sekali, yang jauh lebih cepat.
Marquis of Lorne
di mana tepatnya ringkasan saya salah? Dan apa "ringkasan" untuk memulai? Saya secara eksplisit berbicara tentang "operasi put dari tanah Jawa". Jika Anda hanya menyalin data di antara Saluran (yaitu tidak pernah harus berurusan dengan data di tanah Jawa) itu cerita yang berbeda tentu saja.
Robert Klemme
@RobertKlemme Pernyataan Anda bahwa 'dengan buffer langsung [hanya] semua operasi put dari Jawa harus disilangkan' tidak benar. Keduanya mendapat dan menempatkan harus menyeberang.
Marquis of Lorne
EJP, Anda tampaknya masih kehilangan perbedaan yang dimaksudkan oleh @RobertKlemme dengan memilih untuk menggunakan kata-kata "menempatkan operasi" dalam satu frasa dan menggunakan kata-kata "operasi IO" dalam frasa yang berbeda dari kalimat. Dalam frasa terakhir, maksudnya adalah merujuk pada operasi antara buffer dan perangkat yang disediakan OS dari beberapa jenis.
naki
18

Terbaik untuk melakukan pengukuran Anda sendiri. Jawaban cepat tampaknya bahwa pengiriman dari allocateDirect()buffer membutuhkan waktu 25% hingga 75% lebih sedikit dari allocate()varian (diuji sebagai menyalin file ke / dev / null), tergantung pada ukuran, tetapi alokasi itu sendiri dapat secara signifikan lebih lambat (bahkan oleh faktor 100x).

Sumber:

Raph Levien
sumber
Terima kasih. Saya akan menerima jawaban Anda tetapi saya sedang mencari beberapa detail yang lebih spesifik mengenai perbedaan kinerja.