Praktik terbaik untuk membuat jutaan objek sementara kecil

109

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:

  1. Meminimalkan overhead pengumpulan sampah, dan
  2. 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.

Programmer yang Rendah Hati
sumber
11
Apakah Pola Kelas Terbang sesuai untuk kasus Anda? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland
4
Apakah Anda perlu merangkumnya ke dalam sebuah objek?
nhahtdh
1
Pola Bobot Terbang tidak sesuai, karena objek tidak memiliki data umum yang signifikan. Adapun mengenkapsulasi data dalam suatu objek, itu terlalu besar untuk dikemas menjadi primitif, itulah sebabnya saya mencari alternatif untuk POJO.
Humble Programmer
2
Bacaan yang sangat dianjurkan: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Jawaban:

47

Jalankan aplikasi dengan pengumpulan sampah verbose:

java -verbose:gc

Dan itu akan memberi tahu Anda saat terkumpul. Akan ada dua jenis sapuan, sapuan cepat dan sapuan penuh.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

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.

Niels Bech Nielsen
sumber
Bereksperimenlah dengan ukuran heap Java untuk mencoba menemukan titik di mana pengumpulan sampah penuh jarang terjadi. Di Java 7, GC G1 baru lebih cepat di beberapa kasus (dan lebih lambat di kasus lain).
Michael Shopsin
21

Sejak versi 6, mode server JVM menggunakan teknik analisis escape . Dengan menggunakannya, Anda dapat menghindari GC secara bersamaan.

Mikhail
sumber
1
Analisis melarikan diri sering mengecewakan, ada baiknya memeriksa apakah JVM telah mengetahui apa yang Anda lakukan atau tidak.
Nitsan Wakart
2
Jika Anda memiliki pengalaman menggunakan opsi ini: -XX: + PrintEscapeAnalysis dan -XX: + PrintEliminateAllocations. Itu akan sangat bagus untuk dibagikan. Karena saya tidak, berkata jujur.
Mikhail
lihat stackoverflow.com/questions/9032519/… Anda perlu mendapatkan build debug untuk JDK 7, saya akui saya belum melakukannya tetapi dengan JDK 6, ini telah berhasil.
Nitsan Wakart
19

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

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Jangan berpikir tentang loop unrolling (optimisasi yang dijalankan JVM pada kode Anda). Jika maxsama dengan Integer.MAX_VALUE, perulangan Anda mungkin memerlukan beberapa waktu untuk dieksekusi. Namun, ivariabel 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!

Pierre Laporte
sumber
11

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 ByteBuffersdengan 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 menggunakanfinal bidang karena bidang memerlukan pagar memori pada beberapa platform (yaitu ARM / Daya), namun pada x86 gratis.

bestsss
sumber
8

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:

  • multi-threading: menggunakan kumpulan lokal utas mungkin berfungsi untuk kasus Anda
  • mendukung struktur data: pertimbangkan untuk menggunakan ArrayDeque karena kinerjanya baik saat dihapus dan tidak memiliki overhead alokasi
  • batasi ukuran kolam Anda :)

Mengukur sebelum / sesudah dll, dll

Nitsan Wakart
sumber
6

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.

StanislavL
sumber
Hanya karena minat: Apa yang dibeli dengan kinerja bijaksana? Apakah Anda membuat profil aplikasi Anda sebelum dan sesudah perubahan, dan jika ya, apa hasilnya?
Axel
@Axel objek menggunakan lebih sedikit memori sehingga GC tidak sering dipanggil. Jelas kami membuat profil aplikasi kami, tetapi bahkan ada efek visual dari peningkatan kecepatan.
StanislavL
6

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.

OldCurmudgeon
sumber
2

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.

David Plumpton
sumber
1

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

rkj
sumber
Tautan mati. Apakah ada sumber lain untuk artikel itu?
dnault
0

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. :)

gyorgyabraham.dll
sumber
1
Maaf sobat, saya tidak setuju dengan pendekatan Anda ... Java, seperti bahasa pemrograman lainnya, adalah tentang memecahkan masalah dalam batasannya, jika OP dibatasi oleh GC bagaimana Anda membantunya?
Nitsan Wakart
1
Saya memberi tahu dia bagaimana sebenarnya Java bekerja. Jika dia tidak dapat menghindari situasi memiliki jutaan objek temp, saran terbaiknya adalah, kelas temp harus ringan dan dia harus memastikan bahwa dia merilis referensi secepat mungkin, bukan satu langkah lagi. Apakah saya melewatkan sesuatu?
gyorgyabraham
Java mendukung pembuatan sampah, dan akan membersihkannya untuk Anda, itu benar. Jika OP tidak bisa menghindari pembuatan objek, dan dia tidak senang dengan waktu yang dihabiskan di GC, itu akhir yang menyedihkan. Keberatan saya adalah untuk rekomendasi yang Anda buat untuk membuat lebih banyak pekerjaan untuk GC karena itu adalah Java yang tepat.
Nitsan Wakart
0

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

Lukas 1985
sumber
0

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.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}
Ilya Gazman
sumber
3
Menggunakan kumpulan untuk objek kecil adalah ide yang cukup buruk, Anda memerlukan kumpulan per utas untuk boot (atau akses bersama membunuh kinerja apa pun). Kolam seperti itu juga berkinerja lebih buruk daripada pengumpul sampah yang baik. Terakhir: GC adalah anugerah ketika menangani dengan kode / struktur bersamaan - banyak algoritme secara signifikan lebih mudah diimplementasikan karena secara alami tidak ada masalah ABA. Ref. menghitung dalam lingkungan bersamaan membutuhkan setidaknya operasi atom + pagar memori (LOCK ADD atau CAS pada x86)
bestsss
1
Pengelolaan objek di kumpulan mungkin lebih mahal daripada membiarkan pengumpul sampah berjalan.
Thorbjørn Ravn Andersen
@ ThorbjørnRavnAndersen Secara umum saya setuju dengan Anda, tetapi perhatikan bahwa mendeteksi perbedaan seperti itu cukup menantang, dan ketika Anda sampai pada kesimpulan bahwa GC bekerja lebih baik dalam kasus Anda, itu harus menjadi kasus yang sangat unik jika perbedaan tersebut penting. Bagaimana pun sebaliknya, mungkin saja Object pool akan menyimpan aplikasi Anda.
Ilya Gazman
1
Saya benar-benar tidak mengerti argumen Anda? Sangat sulit untuk mendeteksi apakah GC lebih cepat daripada penggabungan objek? Dan karena itu Anda harus menggunakan penggabungan objek? JVM dioptimalkan untuk pengkodean bersih dan objek berumur pendek. Jika itu pertanyaannya (yang saya harap jika OP menghasilkan sejuta dari mereka pr detik) maka seharusnya hanya jika ada keuntungan yang dapat dibuktikan untuk beralih ke skema yang lebih kompleks dan rawan kesalahan seperti yang Anda sarankan. Jika ini terlalu sulit untuk dibuktikan, lalu mengapa repot-repot.
Thorbjørn Ravn Andersen
0

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.

Michael Röschter
sumber