Apa "praktik terbaik" untuk membuat (dan melepaskan) jutaan objek kecil?
Saya sedang menulis program catur di Java dan algoritme pencarian menghasilkan satu objek "Pindah" untuk setiap gerakan yang mungkin, dan penelusuran nominal dapat dengan mudah menghasilkan lebih dari satu juta objek bergerak per detik. JVM GC telah mampu menangani beban pada sistem pengembangan saya, tetapi saya tertarik untuk mengeksplorasi pendekatan alternatif yang akan:
- Meminimalkan overhead pengumpulan sampah, dan
- mengurangi jejak memori puncak untuk sistem kelas bawah.
Sebagian besar objek berumur sangat pendek, tetapi sekitar 1% gerakan yang dihasilkan dipertahankan dan dikembalikan sebagai nilai yang dipertahankan, sehingga teknik penyatuan atau penyimpanan apa pun harus menyediakan kemampuan untuk mengecualikan objek tertentu agar tidak digunakan kembali .
Saya tidak mengharapkan kode contoh yang lengkap, tetapi saya akan menghargai saran untuk bacaan / penelitian lebih lanjut, atau contoh open source yang serupa.
sumber
Jawaban:
Jalankan aplikasi dengan pengumpulan sampah verbose:
Dan itu akan memberi tahu Anda saat terkumpul. Akan ada dua jenis sapuan, sapuan cepat dan sapuan penuh.
Anak panah adalah ukuran sebelum dan sesudah.
Selama itu hanya melakukan GC dan bukan GC penuh Anda aman di rumah. GC reguler adalah pengumpul salinan di 'generasi muda', jadi objek yang tidak lagi direferensikan akan dilupakan begitu saja, yang persis seperti yang Anda inginkan.
Membaca Java SE 6 HotSpot Virtual Machine Pengumpulan Sampah Tuning mungkin bisa membantu.
sumber
Sejak versi 6, mode server JVM menggunakan teknik analisis escape . Dengan menggunakannya, Anda dapat menghindari GC secara bersamaan.
sumber
Nah, ada beberapa pertanyaan di sini!
1 - Bagaimana cara mengatur objek berumur pendek?
Seperti yang dinyatakan sebelumnya, JVM dapat dengan sempurna menangani sejumlah besar objek berumur pendek, karena mengikuti Hipotesis Generasi Lemah .
Perhatikan bahwa kita berbicara tentang objek yang mencapai memori utama (heap). Ini tidak selalu terjadi. Banyak objek yang Anda buat bahkan tidak meninggalkan register CPU. Misalnya, pertimbangkan ini untuk loop
Jangan berpikir tentang loop unrolling (optimisasi yang dijalankan JVM pada kode Anda). Jika
max
sama denganInteger.MAX_VALUE
, perulangan Anda mungkin memerlukan beberapa waktu untuk dieksekusi. Namun,i
variabel tidak akan pernah lepas dari blok loop. Oleh karena itu, JVM akan meletakkan variabel itu dalam register CPU, menaikkannya secara teratur tetapi tidak akan pernah mengirimnya kembali ke memori utama.Jadi, membuat jutaan objek bukanlah masalah besar jika hanya digunakan secara lokal. Mereka akan mati sebelum disimpan di Eden, jadi GC tidak akan menyadarinya.
2 - Apakah berguna untuk mengurangi overhead GC?
Seperti biasa, itu tergantung.
Pertama, Anda harus mengaktifkan logging GC agar dapat melihat dengan jelas tentang apa yang sedang terjadi. Anda dapat mengaktifkannya dengan
-Xloggc:gc.log -XX:+PrintGCDetails
.Jika aplikasi Anda menghabiskan banyak waktu dalam siklus GC, maka, ya, sesuaikan GC, jika tidak, itu mungkin tidak terlalu berharga.
Misalnya, jika Anda memiliki GC muda setiap 100 md yang membutuhkan 10 md, Anda menghabiskan 10% waktu Anda di GC, dan Anda memiliki 10 koleksi per detik (yang huuuuuge). Dalam kasus seperti itu, saya tidak akan menghabiskan waktu dalam penyetelan GC, karena 10 GC / s itu akan tetap ada.
3 - Beberapa pengalaman
Saya memiliki masalah serupa pada aplikasi yang membuat sejumlah besar kelas tertentu. Di log GC, saya perhatikan bahwa tingkat pembuatan aplikasi sekitar 3 GB / s, yang terlalu banyak (ayolah ... 3 gigabyte data setiap detik?!).
Masalahnya: Terlalu banyak GC yang disebabkan oleh terlalu banyak objek yang dibuat.
Dalam kasus saya, saya memasang profiler memori dan memperhatikan bahwa kelas mewakili persentase besar dari semua objek saya. Saya melacak contoh untuk mengetahui bahwa kelas ini pada dasarnya adalah sepasang boolean yang dibungkus dalam suatu objek. Dalam kasus tersebut, dua solusi tersedia:
Mengolah algoritma sehingga saya tidak mengembalikan sepasang boolean tetapi saya memiliki dua metode yang mengembalikan setiap boolean secara terpisah
Simpan objek dalam cache, dengan mengetahui bahwa hanya ada 4 contoh berbeda
Saya memilih yang kedua, karena berdampak paling kecil pada aplikasi dan mudah diperkenalkan. Butuh beberapa menit untuk menempatkan pabrik dengan cache yang tidak aman untuk thread (saya tidak memerlukan keamanan thread karena pada akhirnya saya hanya akan memiliki 4 contoh berbeda).
Tingkat alokasi turun menjadi 1 GB / dtk, begitu pula frekuensi GC muda (dibagi 3).
Semoga membantu!
sumber
Jika Anda hanya memiliki nilai objek (yaitu, tidak ada referensi ke objek lain) dan sungguh, tetapi maksud saya benar-benar berton-ton dari mereka, Anda dapat menggunakan langsung
ByteBuffers
dengan pengurutan byte asli [yang terakhir ini penting] dan Anda memerlukan beberapa ratus baris kode untuk mengalokasikan / menggunakan kembali + pengambil / penyetel. Getter terlihat mirip denganlong getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}
Itu akan menyelesaikan masalah GC hampir seluruhnya selama Anda hanya mengalokasikan satu kali, yaitu, sebagian besar dan kemudian mengelola objeknya sendiri. Alih-alih referensi, Anda hanya akan memiliki indeks (yaitu,
int
) ke fileByteBuffer
yang harus diteruskan. Anda mungkin perlu menyesuaikan memori sendiri.Tekniknya akan terasa seperti digunakan
C and void*
, tetapi dengan beberapa pembungkus itu bisa diterima. Penurunan kinerja bisa menjadi batasan untuk memeriksa apakah kompilator gagal menghilangkannya. Keuntungan utama adalah lokalitas jika Anda memproses tupel seperti vektor, kurangnya header objek juga mengurangi jejak memori.Selain itu, kemungkinan Anda tidak memerlukan pendekatan seperti itu karena generasi muda dari hampir semua JVM mati secara sepele dan biaya alokasi hanyalah sebuah tonjolan penunjuk. Biaya alokasi bisa sedikit lebih tinggi jika Anda menggunakan
final
bidang karena bidang memerlukan pagar memori pada beberapa platform (yaitu ARM / Daya), namun pada x86 gratis.sumber
Dengan asumsi Anda menemukan GC adalah masalah (seperti yang ditunjukkan orang lain mungkin tidak) Anda akan menerapkan manajemen memori Anda sendiri untuk Anda kasus khusus yaitu kelas yang mengalami churn besar-besaran. Coba penyatuan objek, saya telah melihat kasus di mana itu bekerja dengan cukup baik. Menerapkan kumpulan objek adalah jalur yang telah dilalui dengan baik sehingga tidak perlu mengunjungi kembali di sini, perhatikan:
Mengukur sebelum / sesudah dll, dll
sumber
Saya pernah menemui masalah serupa. Pertama-tama, cobalah untuk memperkecil ukuran benda-benda kecil. Kami memperkenalkan beberapa nilai bidang default yang mereferensikannya di setiap contoh objek.
Misalnya, MouseEvent memiliki referensi ke kelas Point. Kami menyimpan Poin dan mereferensikannya alih-alih membuat instance baru. Hal yang sama untuk, misalnya, string kosong.
Sumber lain adalah beberapa boolean yang diganti dengan satu int dan untuk setiap boolean kami hanya menggunakan satu byte int.
sumber
Saya menangani skenario ini dengan beberapa kode pemrosesan XML beberapa waktu lalu. Saya menemukan diri saya membuat jutaan objek tag XML yang sangat kecil (biasanya hanya string) dan berumur sangat pendek (kegagalan pemeriksaan XPath berarti tidak ada kecocokan jadi buang).
Saya melakukan beberapa pengujian serius dan sampai pada kesimpulan bahwa saya hanya dapat mencapai sekitar 7% peningkatan kecepatan menggunakan daftar tag yang dibuang alih-alih membuat yang baru. Namun, setelah diimplementasikan, saya menemukan bahwa antrian gratis memerlukan mekanisme yang ditambahkan untuk memangkasnya jika terlalu besar - ini benar-benar membatalkan pengoptimalan saya, jadi saya mengalihkannya ke opsi.
Singkatnya - mungkin tidak sepadan - tetapi saya senang melihat Anda memikirkannya, ini menunjukkan bahwa Anda peduli.
sumber
Mengingat Anda sedang menulis program catur, ada beberapa teknik khusus yang dapat Anda gunakan untuk kinerja yang layak. Salah satu pendekatan sederhana adalah membuat array panjang (atau byte) dan memperlakukannya sebagai tumpukan. Setiap kali generator bergerak Anda membuat gerakan, ia mendorong beberapa angka ke tumpukan, misalnya pindah dari kotak dan pindah ke kotak. Saat Anda mengevaluasi pohon pencarian, Anda akan memunculkan gerakan dan memperbarui representasi papan.
Jika Anda ingin daya ekspresif menggunakan objek. Jika Anda ingin kecepatan (dalam hal ini) gunakan yang asli.
sumber
Salah satu solusi yang saya gunakan untuk algoritme penelusuran semacam itu adalah membuat hanya satu objek Pindah, memutasinya dengan gerakan baru, lalu membatalkan pemindahan sebelum meninggalkan ruang lingkup. Anda mungkin menganalisis hanya satu gerakan pada satu waktu, dan kemudian hanya menyimpan langkah terbaik di suatu tempat.
Jika itu tidak layak karena suatu alasan, dan Anda ingin mengurangi penggunaan memori puncak, artikel bagus tentang efisiensi memori ada di sini: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- tutorial.pdf
sumber
Buat saja jutaan objek Anda dan tulis kode Anda dengan cara yang benar: jangan menyimpan referensi yang tidak perlu ke objek ini. GC akan melakukan pekerjaan kotor untuk Anda. Anda dapat bermain-main dengan GC verbose seperti yang disebutkan untuk melihat apakah mereka benar-benar GC. Java IS tentang membuat dan merilis objek. :)
sumber
Saya pikir Anda harus membaca tentang alokasi tumpukan di Jawa dan analisis pelarian.
Karena jika Anda mempelajari lebih dalam topik ini, Anda mungkin menemukan bahwa objek Anda bahkan tidak dialokasikan di heap, dan objek tersebut tidak dikumpulkan oleh GC seperti objek di heap tersebut.
Ada penjelasan wikipedia tentang analisis pelarian, dengan contoh cara kerjanya di Jawa:
http://en.wikipedia.org/wiki/Escape_analysis
sumber
Saya bukan penggemar berat GC, jadi saya selalu mencoba mencari cara untuk mengatasinya. Dalam hal ini saya akan menyarankan menggunakan pola Object Pool :
Idenya adalah untuk menghindari membuat objek baru dengan menyimpannya dalam tumpukan sehingga Anda dapat menggunakannya kembali nanti.
sumber
Kumpulan objek memberikan peningkatan yang luar biasa (terkadang 10x) atas alokasi objek di heap. Tetapi penerapan di atas menggunakan daftar tertaut adalah naif dan salah! Daftar tertaut membuat objek untuk mengelola struktur internalnya yang membatalkan upaya. Sebuah Ringbuffer menggunakan berbagai objek bekerja dengan baik. Dalam contoh give (program catur yang mengatur gerakan) Ringbuffer harus dibungkus menjadi objek pemegang untuk daftar semua gerakan yang dihitung. Hanya referensi objek pemegang bergerak yang akan diedarkan.
sumber