Saya harap pertanyaan ini tidak dianggap terlalu mendasar untuk forum ini, tetapi kita lihat saja nanti. Saya bertanya-tanya bagaimana cara memperbaiki beberapa kode untuk kinerja yang lebih baik yang dijalankan beberapa kali.
Katakanlah saya sedang membuat daftar frekuensi kata, menggunakan Peta (mungkin HashMap), di mana setiap kunci adalah String dengan kata yang sedang dihitung dan nilainya adalah Integer yang bertambah setiap kali token kata ditemukan.
Dalam Perl, menambahkan nilai seperti itu akan mudah:
$map{$word}++;
Tetapi di Jawa, ini jauh lebih rumit. Di sini cara saya saat ini melakukannya:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
Yang tentu saja bergantung pada fitur autoboxing di versi Java yang lebih baru. Saya ingin tahu apakah Anda dapat menyarankan cara yang lebih efisien untuk meningkatkan nilai seperti itu. Apakah ada alasan kinerja yang baik untuk menghindari kerangka kerja Koleksi dan menggunakan sesuatu yang lain sebagai gantinya?
Pembaruan: Saya telah melakukan tes beberapa jawaban. Lihat di bawah.
sumber
Jawaban:
Beberapa hasil tes
Saya mendapatkan banyak jawaban yang bagus untuk pertanyaan ini - terima kasih semuanya - jadi saya memutuskan untuk menjalankan beberapa tes dan mencari tahu metode mana yang sebenarnya tercepat. Lima metode yang saya uji adalah:
metode
Inilah yang saya lakukan ...
Hasil
Saya akan mempresentasikan hasil pertama dan kode di bawah ini untuk mereka yang tertarik.
The ContainsKey metode itu, seperti yang diharapkan, paling lambat, jadi saya akan memberikan kecepatan setiap metode dibandingkan dengan kecepatan metode tersebut.
Kesimpulan
Tampaknya hanya metode MutableInt dan metode Trove yang secara signifikan lebih cepat, hanya mereka yang memberikan peningkatan kinerja lebih dari 10%. Namun, jika threading adalah masalah, AtomicLong mungkin lebih menarik daripada yang lain (saya tidak begitu yakin). Saya juga menjalankan TestForNull dengan
final
variabel, tetapi perbedaannya dapat diabaikan.Perhatikan bahwa saya belum membuat profil penggunaan memori dalam berbagai skenario. Saya akan senang mendengar dari siapa pun yang memiliki wawasan yang baik tentang bagaimana metode MutableInt dan Trove akan mempengaruhi penggunaan memori.
Secara pribadi, saya menemukan metode MutableInt yang paling menarik, karena tidak perlu memuat kelas pihak ketiga. Jadi, kecuali saya menemukan masalah dengan itu, itulah cara saya kemungkinan besar pergi.
Kode
Berikut adalah kode penting dari setiap metode.
Berisi kunci
TestForNull
AtomicLong
Harta karun
MutableInt
sumber
freq.compute(word, (key, count) -> count == null ? 1 : count + 1)
? Secara internal ia melakukan pencarian yang kurang hash daripadacontainsKey
, akan menarik untuk melihat bagaimana membandingkannya dengan yang lain, karena lambda.Sekarang ada cara yang lebih pendek dengan Java 8 menggunakan
Map::merge
.Apa fungsinya:
Informasi lebih lanjut di sini .
sumber
map.merge(key, 1, (a, b) -> a + b);
berhasilInteger::sum
sebagai BiFunction, dan tidak suka @russter menjawab cara itu ditulis. Ini berhasil untuk sayaMap.merge(key, 1, { a, b -> a + b})
Sebuah penelitian kecil pada tahun 2016: https://github.com/leventov/java-word-count , kode sumber patokan
Hasil terbaik per metode (lebih kecil lebih baik):
Waktu \ ruang hasil:
sumber
Google Jambu adalah teman Anda ...
... setidaknya dalam beberapa kasus. Mereka memiliki AtomicLongMap yang bagus ini . Terutama baik karena Anda berurusan dengan lama sebagai nilai di peta Anda.
Misalnya
Juga dimungkinkan untuk menambahkan lebih dari 1 ke nilai:
sumber
AtomicLongMap#getAndAdd
mengambil kelas primitiflong
dan bukan kelas pembungkus; tidak ada gunanya melakukannew Long()
. DanAtomicLongMap
merupakan tipe parameter; Anda seharusnya menyatakannya sebagaiAtomicLongMap<String>
.@Hank Gay
Sebagai tindak lanjut dari komentar saya (yang agak tidak berguna): Trove terlihat seperti cara untuk pergi. Jika, untuk alasan apapun, Anda ingin tetap dengan JDK standar, ConcurrentMap dan AtomicLong dapat membuat kode kecil sedikit lebih bagus, meskipun YMMV.
akan meninggalkan
1
nilai di peta untukfoo
. Secara realistis, peningkatan keramahan terhadap threading adalah semua yang harus direkomendasikan oleh pendekatan ini.sumber
Dan itulah bagaimana Anda menambah nilai dengan kode sederhana.
Manfaat:
Kelemahan:
Secara teoritis, setelah Anda memanggil get (), Anda sudah tahu ke mana harus meletakkan (), jadi Anda tidak perlu mencari lagi. Tetapi mencari di peta hash biasanya membutuhkan waktu yang sangat minimal sehingga Anda dapat mengabaikan masalah kinerja ini.
Tetapi jika Anda sangat serius tentang masalah ini, Anda perfeksionis, cara lain adalah menggunakan metode penggabungan, ini (mungkin) lebih efisien daripada potongan kode sebelumnya karena Anda akan (secara teoritis) mencari peta hanya sekali: (meskipun kode ini tidak jelas dari pandangan pertama, pendek dan performan)
Saran: Anda harus lebih memperhatikan pembacaan kode lebih dari sedikit peningkatan kinerja di sebagian besar waktu. Jika potongan kode pertama lebih mudah untuk Anda pahami maka gunakanlah. Tetapi jika Anda dapat memahami yang ke-2 baik-baik saja maka Anda juga bisa melakukannya!
sumber
Itu selalu merupakan ide yang baik untuk melihat Perpustakaan Koleksi Google untuk hal semacam ini. Dalam hal ini Multiset akan melakukan trik:
Ada metode seperti Peta untuk mengulangi kunci / entri, dll. Secara internal implementasi saat ini menggunakan a
HashMap<E, AtomicInteger>
, sehingga Anda tidak akan dikenakan biaya tinju.sumber
count()
metode pada multiset berjalan dalam waktu O (1) atau O (n) (terburuk)? Dokumen tidak jelas tentang hal ini.Anda harus menyadari fakta bahwa upaya awal Anda
mengandung dua operasi yang berpotensi mahal pada peta, yaitu
containsKey
danget
. Yang pertama melakukan operasi yang berpotensi sangat mirip dengan yang terakhir, jadi Anda melakukan pekerjaan yang sama dua kali !Jika Anda melihat API untuk Peta,
get
operasi biasanya kembalinull
ketika peta tidak mengandung elemen yang diminta.Perhatikan bahwa ini akan membuat solusi seperti
berbahaya, karena mungkin menghasilkan
NullPointerException
s. Anda harus memeriksanull
dulu.Juga perhatikan , dan ini sangat penting, bahwa
HashMap
s dapat mengandungnulls
dengan definisi. Jadi tidak setiap kembalinull
mengatakan "tidak ada elemen seperti itu". Dalam hal ini,containsKey
berperilaku berbeda dariget
dalam benar-benar memberitahu Anda apakah ada elemen seperti itu. Lihat API untuk detailnya.Namun, untuk kasus Anda, Anda mungkin tidak ingin membedakan antara yang tersimpan
null
dan "noSuchElement". Jika Anda tidak ingin mengizinkannull
, Anda mungkin memilih aHashtable
. Menggunakan perpustakaan pembungkus seperti yang sudah diusulkan dalam jawaban lain mungkin merupakan solusi yang lebih baik untuk perawatan manual, tergantung pada kompleksitas aplikasi Anda.Untuk menyelesaikan jawaban (dan saya lupa memasukkannya pada awalnya, berkat fungsi edit!), Cara terbaik untuk melakukannya secara asli, adalah ke
get
dalamfinal
variabel, periksanull
danput
kembali dengan a1
. Variabelnya harusfinal
karena tetap tidak berubah. Kompilator mungkin tidak memerlukan petunjuk ini, tetapi lebih jelas seperti itu.Jika Anda tidak ingin mengandalkan autoboxing, Anda harus mengatakan sesuatu seperti itu
map.put(new Integer(1 + i.getValue()));
.sumber
Cara lain akan membuat integer yang bisa berubah:
tentu saja ini menyiratkan membuat objek tambahan tetapi overhead dibandingkan dengan membuat Integer (bahkan dengan Integer.valueOf) seharusnya tidak terlalu banyak.
sumber
Anda dapat menggunakan metode computeIfAbsent di
Map
antarmuka yang disediakan di Java 8 .Metode ini
computeIfAbsent
memeriksa apakah kunci yang ditentukan sudah dikaitkan dengan nilai atau tidak? Jika tidak ada nilai terkait maka ia mencoba menghitung nilainya menggunakan fungsi pemetaan yang diberikan. Dalam setiap kasus itu mengembalikan nilai saat ini (yang ada atau dihitung) terkait dengan kunci yang ditentukan, atau nol jika nilai yang dihitung adalah nol.Di samping catatan jika Anda memiliki situasi di mana beberapa utas memperbarui jumlah umum Anda dapat melihat pada kelas LongAdder. Di bawah pertikaian tinggi, throughput yang diharapkan dari kelas ini secara signifikan lebih tinggi daripada
AtomicLong
, dengan mengorbankan konsumsi ruang yang lebih tinggi.sumber
Rotasi memori dapat menjadi masalah di sini, karena setiap tinju int yang lebih besar dari atau sama dengan 128 menyebabkan alokasi objek (lihat Integer.valueOf (int)). Meskipun pengumpul sampah sangat efisien menangani benda-benda berumur pendek, kinerja akan sedikit menurun.
Jika Anda tahu bahwa jumlah peningkatan yang dilakukan sebagian besar akan melebihi jumlah kunci (= kata dalam hal ini), pertimbangkan menggunakan int holder sebagai gantinya. Phax sudah menyajikan kode untuk ini. Ini dia lagi, dengan dua perubahan (kelas pemegang dibuat statis dan nilai awal diatur ke 1):
Jika Anda membutuhkan kinerja ekstrem, cari implementasi Peta yang langsung disesuaikan dengan tipe nilai primitif. jrudolph menyebut GNU Trove .
Omong-omong, istilah pencarian yang bagus untuk subjek ini adalah "histogram".
sumber
Alih-alih memanggil containKey () lebih cepat hanya untuk memanggil map.get dan periksa apakah nilai yang dikembalikan adalah nol atau tidak.
sumber
Apakah Anda yakin ini adalah hambatan? Sudahkah Anda melakukan analisis kinerja?
Coba gunakan profiler NetBeans (gratis dan dibangun di NB 6.1) untuk melihat hotspot.
Akhirnya, peningkatan JVM (katakanlah dari 1,5-> 1,6) seringkali merupakan penambah kinerja yang murah. Bahkan peningkatan jumlah build dapat memberikan peningkatan kinerja yang baik. Jika Anda menjalankan pada Windows dan ini adalah aplikasi kelas server, gunakan -server pada baris perintah untuk menggunakan Server Hotspot JVM. Pada mesin Linux dan Solaris ini terdeteksi secara otomatis.
sumber
Ada beberapa pendekatan:
Gunakan aloritma Bag seperti set yang terkandung dalam Google Collections.
Buat wadah yang bisa berubah yang dapat Anda gunakan di Peta:
Dan gunakan put ("word", new My ("Word")); Kemudian Anda dapat memeriksa apakah ada dan bertambah saat menambahkan.
Hindari menggulung solusi Anda sendiri menggunakan daftar, karena jika Anda mencari dan menyortir innerloop, kinerja Anda akan berbau busuk. Solusi HashMap pertama sebenarnya cukup cepat, tetapi yang tepat seperti yang ditemukan di Google Collections mungkin lebih baik.
Menghitung kata menggunakan Google Collections, terlihat seperti ini:
Menggunakan HashMultiset cukup elegan, karena bag-algoritme hanya yang Anda butuhkan saat menghitung kata.
sumber
Saya pikir solusi Anda akan menjadi cara standar, tetapi - seperti yang Anda catat sendiri - itu mungkin bukan cara tercepat yang mungkin.
Anda dapat melihat GNU Trove . Itu adalah perpustakaan yang berisi segala macam Koleksi primitif cepat. Contoh Anda akan menggunakan TObjectIntHashMap yang memiliki metode sesuaikanOrPutValue yang melakukan persis apa yang Anda inginkan.
sumber
Variasi pada pendekatan MutableInt yang mungkin lebih cepat, jika sedikit peretasan, adalah dengan menggunakan array int elemen tunggal:
Akan menarik jika Anda dapat menjalankan kembali tes kinerja Anda dengan variasi ini. Mungkin yang tercepat.
Sunting: Pola di atas bekerja dengan baik untuk saya, tetapi akhirnya saya berubah untuk menggunakan koleksi Trove untuk mengurangi ukuran memori di beberapa peta yang sangat besar yang saya buat - dan sebagai bonus itu juga lebih cepat.
Salah satu fitur yang sangat bagus adalah bahwa
TObjectIntHashMap
kelas memiliki satuadjustOrPutValue
panggilan itu, tergantung pada apakah sudah ada nilai pada kunci itu, apakah akan memasukkan nilai awal atau menambah nilai yang ada. Ini sempurna untuk menambah:sumber
Google Collections HashMultiset:
- cukup elegan untuk digunakan
- tetapi mengonsumsi CPU dan memori
Yang terbaik adalah memiliki metode seperti:
Entry<K,V> getOrPut(K);
(elegan, dan biaya rendah)Metode seperti itu akan menghitung hash dan indeks hanya sekali, dan kemudian kita bisa melakukan apa yang kita inginkan dengan entri (baik mengganti atau memperbarui nilainya).
Lebih elegan:
- ambil a
HashSet<Entry>
- rentangkan sehingga
get(K)
letakkan Entri baru jika diperlukan- Entri bisa menjadi objek Anda sendiri.
->
(new MyHashSet()).get(k).increment();
sumber
Cukup sederhana, cukup gunakan fungsi
Map.java
bawaan sebagai berikutsumber
++
... OMG, ini sangat sederhana. @siegi++
tidak bekerja di mana saja dalam ekspresi ini karena variabel diperlukan sebagai operan tetapi hanya ada nilai. Penambahan Anda+ 1
bekerja meskipun. Sekarang solusi Anda sama dengan jawaban off99555s ."put" need "get" (untuk memastikan tidak ada kunci duplikat).
Jadi langsung lakukan "put",
dan jika ada nilai sebelumnya, maka lakukan penambahan:
Jika hitungan dimulai dari 0, maka tambahkan 1: (atau nilai lainnya ...)
Perhatikan: Kode ini tidak aman untuk thread. Gunakan untuk membangun lalu gunakan peta, bukan untuk memperbaruinya secara bersamaan.
Optimasi: Dalam satu lingkaran, pertahankan nilai lama untuk menjadi nilai baru dari loop berikutnya.
sumber
Berbagai pembungkus primitif, misalnya,
Integer
tidak berubah sehingga benar-benar tidak ada cara yang lebih ringkas untuk melakukan apa yang Anda minta kecuali Anda dapat melakukannya dengan sesuatu seperti AtomicLong . Saya bisa mencobanya sebentar lagi dan memperbarui. BTW, Hashtable adalah bagian dari Collections Framework .sumber
Saya akan menggunakan Apache Collections Lazy Map (untuk menginisialisasi nilai ke 0) dan menggunakan MutableIntegers dari Apache Lang sebagai nilai di peta itu.
Biaya terbesar adalah harus menyisir peta dua kali dalam metode Anda. Di tangan saya, Anda harus melakukannya sekali saja. Dapatkan saja nilainya (akan diinisialisasi jika tidak ada) dan tambahkan.
sumber
Itu Fungsional Java perpustakaan
TreeMap
datastructure memilikiupdate
metode dalam kepala batang terbaru:Contoh penggunaan:
Program ini mencetak "2".
sumber
@Vantantas Baranauskas: Mengenai jawaban ini, saya akan berkomentar jika saya memiliki poin rep, tapi saya tidak. Saya ingin mencatat bahwa kelas Counter didefinisikan TIDAK ada thread-safe karena tidak cukup hanya menyinkronkan inc () tanpa nilai sinkronisasi (). Nilai panggilan utas lainnya () tidak dijamin untuk melihat nilainya kecuali hubungan yang terjadi sebelum hubungan telah terjadi dengan pembaruan.
sumber
Saya tidak tahu seberapa efisien itu tetapi kode di bawah ini berfungsi juga. Anda harus mendefinisikan
BiFunction
di awal. Plus, Anda dapat membuat lebih dari sekadar peningkatan dengan metode ini.output adalah
sumber
Jika Anda menggunakan Eclipse Collections , Anda dapat menggunakan a
HashBag
. Ini akan menjadi pendekatan yang paling efisien dalam hal penggunaan memori dan juga akan bekerja dengan baik dalam hal kecepatan eksekusi.HashBag
didukung olehMutableObjectIntMap
yang menyimpan int primitif bukanCounter
objek. Ini mengurangi overhead memori dan meningkatkan kecepatan eksekusi.HashBag
menyediakan API yang Anda perlukan karena itu aCollection
yang juga memungkinkan Anda untuk menanyakan jumlah kemunculan suatu item.Berikut adalah contoh dari Eclipse Collections Kata .
Catatan: Saya pengendara untuk Eclipse Collections.
sumber
Saya sarankan untuk menggunakan Java 8 Map :: compute (). Itu mempertimbangkan kasus ketika kunci tidak ada juga.
sumber
mymap.merge(key, 1, Integer::sum)
?Karena banyak orang mencari topik Java untuk jawaban Groovy, berikut ini cara melakukannya di Groovy:
sumber
Cara sederhana dan mudah di java 8 adalah sebagai berikut:
sumber
Semoga saya mengerti pertanyaan Anda dengan benar, saya datang ke Jawa dari Python sehingga saya bisa berempati dengan perjuangan Anda.
jika Anda memiliki
kamu akan lakukan
Semoga ini membantu!
sumber