Apakah Java jauh lebih sulit untuk "tweak" untuk kinerja dibandingkan dengan C / C ++? [Tutup]

11

Apakah "keajaiban" JVM menghalangi pengaruh yang dimiliki seorang programmer terhadap optimisasi mikro di Jawa? Saya baru-baru ini membaca di C ++ kadang-kadang pemesanan anggota data dapat memberikan optimasi (diberikan, dalam lingkungan mikrodetik) dan saya kira tangan seorang programmer terikat ketika datang untuk memeras kinerja dari Jawa?

Saya menghargai algoritma yang layak memberikan peningkatan kecepatan yang lebih besar, tetapi begitu Anda memiliki algoritma yang benar, apakah Java lebih sulit untuk di-tweak karena kontrol JVM?

Jika tidak, bisakah orang memberikan contoh trik apa yang dapat Anda gunakan di Java (selain flag compiler sederhana).

pengguna997112
sumber
14
Prinsip dasar di balik semua optimasi Java adalah ini: JVM mungkin sudah melakukannya lebih baik daripada yang Anda bisa. Optimasi sebagian besar melibatkan mengikuti praktik pemrograman yang masuk akal dan menghindari hal-hal biasa seperti merangkai string dalam satu lingkaran.
Robert Harvey
3
Prinsip optimasi mikro dalam semua bahasa adalah bahwa kompiler sudah melakukannya dengan lebih baik daripada yang Anda bisa. Prinsip lain optimasi mikro dalam semua bahasa adalah bahwa melemparkan lebih banyak perangkat keras lebih murah daripada waktu programmer mengoptimalkan mikro. Programmer harus cenderung untuk menskala masalah (algoritma suboptimal), tetapi optimasi mikro adalah buang-buang waktu. Kadang-kadang optimasi mikro masuk akal pada sistem tertanam di mana Anda tidak dapat membuang lebih banyak perangkat keras di atasnya, tetapi Android menggunakan Java, dan implementasi yang agak buruk, menunjukkan sebagian besar dari mereka sudah memiliki cukup perangkat keras.
Jan Hudec
1
untuk "trik kinerja Java", senilai belajar adalah: Java Efektif , Angelika Langer Links - Kinerja Java dan artikel kinerja terkait dengan Brian Goetz di Java teori dan praktek dan Threading Ringan seri terdaftar di sini
nyamuk
2
Berhati-hatilah dengan tip dan trik - JVM, sistem operasi dan peranti keras bergerak - Anda sebaiknya mempelajari metodologi penyempurnaan kinerja dan menerapkan penyempurnaan untuk lingkungan khusus Anda :-)
Martijn Verburg
Dalam beberapa kasus, VM dapat membuat optimasi pada saat run time yang tidak praktis untuk dibuat pada waktu kompilasi. Menggunakan memori yang dikelola dapat meningkatkan kinerja, meskipun itu juga akan sering memiliki jejak memori yang lebih tinggi. Memori yang tidak digunakan dibebaskan saat nyaman, alih-alih secepatnya.
Brian

Jawaban:

5

Tentu saja, pada tingkat optimasi mikro, JVM akan melakukan beberapa hal yang Anda akan memiliki sedikit kontrol dibandingkan dengan C dan C ++ terutama.

Di sisi lain, variasi perilaku kompiler dengan C dan C ++ terutama akan memiliki dampak negatif yang jauh lebih besar pada kemampuan Anda untuk melakukan optimasi mikro dalam segala cara yang agak portabel (bahkan lintas revisi kompiler).

Itu tergantung pada jenis proyek apa yang Anda sesuaikan, lingkungan apa yang Anda targetkan dan sebagainya. Dan pada akhirnya, itu tidak terlalu penting karena Anda mendapatkan beberapa pesanan dengan hasil yang lebih baik dari optimisasi algoritmik / struktur data / program.

Telastyn
sumber
Ini bisa sangat berarti ketika Anda menemukan aplikasi Anda tidak berskala lintas core
James
@james - peduli untuk menjelaskan?
Telastyn
1
@Ames, scaling lintas core sangat sedikit hubungannya dengan bahasa implementasi (Python dikecualikan!), Dan, lebih berkaitan dengan arsitektur aplikasi.
James Anderson
29

Optimalisasi mikro hampir tidak pernah sepadan dengan waktu, dan hampir semua yang mudah dilakukan secara otomatis oleh kompiler dan runtime.

Namun, ada satu bidang optimasi yang penting di mana C ++ dan Java berbeda secara mendasar, dan itu adalah akses memori massal. C ++ memiliki manajemen memori manual, yang berarti Anda dapat mengoptimalkan tata letak data aplikasi dan pola akses untuk memanfaatkan cache secara penuh. Ini cukup sulit, agak spesifik untuk perangkat keras yang Anda jalankan (sehingga peningkatan kinerja dapat menghilang pada perangkat keras yang berbeda), tetapi jika dilakukan dengan benar, itu dapat menyebabkan kinerja yang benar-benar menakjubkan. Tentu saja Anda membayarnya dengan potensi untuk semua jenis bug yang mengerikan.

Dengan bahasa sampah yang dikumpulkan seperti Java, optimisasi semacam ini tidak dapat dilakukan dalam kode. Beberapa dapat dilakukan dengan runtime (secara otomatis atau melalui konfigurasi, lihat di bawah), dan beberapa tidak mungkin (harga yang Anda bayar untuk dilindungi dari bug manajemen memori).

Jika tidak, bisakah orang memberikan contoh trik apa yang dapat Anda gunakan di Java (selain flag compiler sederhana).

Bendera kompiler tidak relevan di Jawa karena kompiler Java hampir tidak ada optimasi; runtime tidak.

Dan memang Java runtimes memiliki banyak parameter yang dapat disesuaikan, terutama mengenai pengumpul sampah. Tidak ada yang "sederhana" tentang opsi-opsi itu - standarnya bagus untuk sebagian besar aplikasi, dan mendapatkan kinerja yang lebih baik mengharuskan Anda untuk memahami dengan tepat apa yang dilakukan opsi dan bagaimana aplikasi Anda berperilaku.

Michael Borgwardt
sumber
1
+1: pada dasarnya apa yang saya tulis dalam jawaban saya, mungkin formulasi yang lebih baik.
Klaim
1
+1: Poin yang sangat bagus, dijelaskan dengan cara yang sangat ringkas: "Ini sangat sulit ... tetapi jika dilakukan dengan benar, itu dapat menyebabkan kinerja yang benar-benar menakjubkan. Tentu saja Anda membayarnya dengan potensi untuk semua jenis bug yang mengerikan. . "
Giorgio
1
@MartinBa: Lebih dari yang Anda bayar untuk mengoptimalkan manajemen memori. Jika Anda tidak mencoba untuk mengoptimalkan manajemen memori, manajemen memori C ++ tidak terlalu sulit (hindari sepenuhnya melalui STL atau membuatnya lebih mudah menggunakan RAII). Tentu saja, mengimplementasikan RAII di C ++ membutuhkan lebih banyak baris kode daripada tidak melakukan apa pun di Jawa (yaitu, karena Java menanganinya untuk Anda).
Brian
3
@ Martin Ba: Pada dasarnya ya. Pointer menggantung, buffer overflows, pointer tidak diinisialisasi, kesalahan dalam aritmatika pointer, semua hal yang tidak ada tanpa manajemen memori manual. Dan mengoptimalkan akses memori cukup banyak mengharuskan Anda melakukan banyak manajemen memori manual.
Michael Borgwardt
1
Ada beberapa hal yang dapat Anda lakukan di java. Salah satunya adalah penyatuan objek, yang memaksimalkan peluang memori lokalitas objek (tidak seperti C ++ yang dapat menjamin lokalitas memori).
RokL
5

[...] (diberikan, dalam lingkungan mikrodetik) [...]

Mikro-detik bertambah jika kita menghasilkan jutaan hingga milyaran hal. Sesi optimisasi vtune / mikro pribadi dari C ++ (tidak ada peningkatan algoritmik):

T-Rex (12.3 million facets):
Initial Time: 32.2372797 seconds
Multithreading: 7.4896073 seconds
4.9201039 seconds
4.6946372 seconds
3.261677 seconds
2.6988536 seconds
SIMD: 1.7831 seconds
4-valence patch optimization: 1.25007 seconds
0.978046 seconds
0.970057 seconds
0.911041 seconds

Semuanya selain "multithreading", "SIMD" (tulisan tangan untuk mengalahkan kompiler), dan optimasi patch 4-valensi adalah optimasi memori level mikro. Juga kode asli mulai dari waktu awal 32 detik sudah dioptimalkan sedikit (kompleksitas algoritmik yang optimal secara teoritis) dan ini adalah sesi baru-baru ini. Versi asli jauh sebelum sesi terakhir ini membutuhkan waktu 5 menit untuk diproses.

Mengoptimalkan efisiensi memori dapat sering membantu di mana saja dari beberapa kali hingga urutan besarnya dalam konteks single-threaded, dan lebih banyak lagi dalam konteks multithreaded (manfaat dari rep memori yang efisien sering kali berlipat ganda dengan banyak utas dalam campuran).

Tentang Pentingnya Optimalisasi Mikro

Saya sedikit gelisah dengan gagasan bahwa optimasi mikro adalah buang-buang waktu. Saya setuju bahwa itu adalah saran umum yang baik, tetapi tidak semua orang melakukannya secara salah berdasarkan firasat dan takhayul daripada pengukuran. Dilakukan dengan benar, itu tidak selalu menghasilkan dampak mikro. Jika kita menggunakan Embree Intel sendiri (raytracing kernel) dan hanya menguji BVH skalar sederhana yang telah mereka tulis (bukan paket ray yang secara eksponensial sulit dikalahkan), dan kemudian mencoba mengalahkan kinerja struktur data itu, itu bisa menjadi yang paling pengalaman merendahkan bahkan untuk seorang veteran yang digunakan untuk profil dan tuning kode selama beberapa dekade. Dan itu semua karena optimasi mikro diterapkan. Solusi mereka dapat memproses lebih dari seratus juta sinar per detik ketika saya melihat profesional industri bekerja dalam raytracing yang bisa

Tidak ada cara untuk mengambil implementasi langsung dari BVH dengan hanya fokus algoritmik dan mendapatkan lebih dari seratus juta persimpangan sinar primer per detik dari itu terhadap kompiler pengoptimalisasi (bahkan ICC milik Intel sendiri). Yang mudah seringkali bahkan tidak mendapatkan sejuta sinar per detik. Dibutuhkan solusi berkualitas profesional untuk sering bahkan mendapatkan beberapa juta sinar per detik. Diperlukan optimasi mikro tingkat Intel untuk mendapatkan lebih dari seratus juta sinar per detik.

Algoritma

Saya pikir optimasi mikro tidak penting selama kinerja tidak penting pada level menit ke detik, misalnya, atau jam ke menit. Jika kita mengambil algoritma yang mengerikan seperti bubble sort dan menggunakannya pada input massa sebagai contoh, dan kemudian membandingkannya dengan bahkan implementasi dasar dari semacam penggabungan, yang pertama mungkin membutuhkan waktu berbulan-bulan untuk diproses, yang terakhir mungkin 12 menit, sebagai hasilnya kompleksitas kuadrat vs linearitmik.

Perbedaan antara bulan dan menit mungkin akan membuat kebanyakan orang, bahkan mereka yang tidak bekerja di bidang kritis kinerja, menganggap waktu eksekusi tidak dapat diterima jika mengharuskan pengguna menunggu berbulan-bulan untuk mendapatkan hasil.

Sementara itu, jika kita membandingkan jenis penggabungan non-mikro yang dioptimalkan, langsung ke quicksort (yang sama sekali tidak unggul secara algoritmik untuk menggabungkan jenis, dan hanya menawarkan peningkatan tingkat mikro untuk lokalitas referensi), quicksort yang dioptimalkan mikro mungkin selesai di 15 detik dibandingkan dengan 12 menit. Membuat pengguna menunggu 12 menit mungkin bisa diterima (semacam coffee break).

Saya pikir perbedaan ini mungkin diabaikan bagi kebanyakan orang antara, katakanlah, 12 menit dan 15 detik, dan itulah sebabnya optimasi mikro sering dianggap tidak berguna karena sering kali hanya seperti perbedaan antara menit dan detik, dan bukan menit dan bulan. Alasan lain saya pikir itu dianggap tidak berguna adalah bahwa itu sering diterapkan pada area yang tidak penting: beberapa area kecil yang bahkan tidak gila dan kritis yang menghasilkan beberapa perbedaan 1% yang dipertanyakan (yang mungkin hanya noise). Tetapi bagi orang-orang yang peduli tentang perbedaan jenis waktu ini dan bersedia untuk mengukur dan melakukannya dengan benar, saya pikir ada baiknya memperhatikan setidaknya konsep dasar hierarki memori (khususnya tingkat atas yang berkaitan dengan kesalahan halaman dan kesalahan cache) .

Java Meninggalkan Banyak Ruang untuk Optimalisasi Mikro yang Baik

Fiuh, maaf - dengan kata-kata kasar semacam itu:

Apakah "keajaiban" JVM menghalangi pengaruh yang dimiliki seorang programmer terhadap optimisasi mikro di Jawa?

Sedikit tetapi tidak sebanyak yang orang pikirkan jika Anda melakukannya dengan benar. Misalnya, jika Anda melakukan pemrosesan gambar, dalam kode asli dengan SIMD tulisan tangan, multithreading, dan optimalisasi memori (pola akses dan mungkin bahkan representasi tergantung pada algoritma pemrosesan gambar), mudah untuk mengolah ratusan juta piksel per detik selama 32- bit RGBA piksel (saluran warna 8-bit) dan kadang-kadang bahkan miliaran per detik.

Mustahil untuk mendekati Java jika Anda mengatakan, membuat Pixelobjek (ini saja akan mengembang ukuran piksel dari 4 byte menjadi 16 pada 64-bit).

Tetapi Anda mungkin bisa mendapatkan jauh lebih dekat jika Anda menghindari Pixelobjek, menggunakan array byte, dan memodelkan Imageobjek. Java masih cukup kompeten di sana jika Anda mulai menggunakan array data lama biasa. Saya sudah mencoba hal-hal semacam ini sebelumnya di Jawa dan cukup terkesan asalkan Anda tidak membuat banyak objek kecil di mana-mana yang 4 kali lebih besar dari biasanya (mis: gunakan intalih-alih Integer) dan mulai memodelkan antarmuka massal seperti Imageantarmuka, bukan Pixelantarmuka. Saya bahkan berani mengatakan bahwa Java dapat menyaingi kinerja C ++ jika Anda mengulang data lama dan bukan objek (array besar float, misalnya, tidak Float).

Mungkin bahkan lebih penting daripada ukuran memori adalah bahwa array intjaminan representasi yang berdekatan. Array Integertidak. Kedekatan seringkali penting untuk lokalitas referensi karena itu berarti banyak elemen (mis: 16 ints) semuanya dapat masuk ke dalam satu baris cache dan berpotensi diakses bersama sebelum penggusuran dengan pola akses memori yang efisien. Sementara itu satu Integermungkin terdampar di suatu tempat dalam memori dengan memori sekitarnya menjadi tidak relevan, hanya untuk memiliki wilayah memori dimuat ke dalam garis cache hanya untuk menggunakan satu bilangan bulat sebelum penggusuran yang bertentangan dengan 16 bilangan bulat. Bahkan jika kita beruntung dan sekitarnya luar biasaIntegersbaik-baik saja di samping satu sama lain dalam memori, kita hanya bisa memasukkan 4 ke dalam garis cache yang dapat diakses sebelum penggusuran karena Integermenjadi 4 kali lebih besar, dan itu dalam skenario kasus terbaik.

Dan ada banyak optimasi mikro yang bisa didapat di sana karena kita disatukan di bawah arsitektur / hierarki memori yang sama. Pola akses memori penting apa pun bahasa yang Anda gunakan, konsep seperti loop tiling / blocking mungkin secara umum diterapkan jauh lebih sering di C atau C ++, tetapi mereka juga menguntungkan Java.

Saya baru-baru ini membaca di C ++ kadang-kadang pemesanan anggota data dapat memberikan optimasi [...]

Urutan anggota data umumnya tidak masalah di Jawa, tapi itu kebanyakan hal yang baik. Dalam C dan C ++, menjaga urutan anggota data sering penting karena alasan ABI sehingga kompiler tidak mengacaukannya. Pengembang manusia yang bekerja di sana harus berhati-hati untuk melakukan hal-hal seperti mengatur anggota data mereka dalam urutan menurun (terbesar ke terkecil) untuk menghindari pemborosan memori pada bantalan. Dengan Java, tampaknya JIT dapat menyusun ulang anggota untuk Anda dengan cepat untuk memastikan keselarasan yang tepat sambil meminimalkan bantalan, jadi asalkan itu terjadi, itu mengotomatiskan sesuatu yang rata-rata yang dapat dilakukan oleh programmer C dan C ++ yang buruk dan akhirnya membuang-buang memori dengan cara itu ( yang tidak hanya membuang-buang memori, tetapi sering membuang-buang kecepatan dengan meningkatkan langkah antara struktur AoS yang tidak perlu dan menyebabkan lebih banyak cache yang hilang). Itu' adalah hal yang sangat robot untuk mengatur ulang bidang untuk meminimalkan bantalan, jadi idealnya manusia tidak berurusan dengan itu. Satu-satunya waktu di mana pengaturan bidang mungkin penting dengan cara yang mengharuskan manusia untuk mengetahui pengaturan optimal adalah jika objek lebih besar dari 64 byte dan kami mengatur bidang berdasarkan pola akses (bukan bantalan optimal) - dalam hal ini mungkin merupakan upaya yang lebih manusiawi (membutuhkan pemahaman jalur kritis, beberapa di antaranya adalah informasi yang tidak mungkin diantisipasi oleh penyusun tanpa mengetahui apa yang akan dilakukan pengguna dengan perangkat lunak).

Jika tidak, bisakah orang memberikan contoh trik apa yang dapat Anda gunakan di Java (selain flag compiler sederhana).

Perbedaan terbesar bagi saya dalam hal mengoptimalkan mentalitas antara Java dan C ++ adalah bahwa C ++ memungkinkan Anda untuk menggunakan objek sedikit (lebih kecil) sedikit lebih banyak daripada Java dalam skenario kinerja-kritis. Sebagai contoh, C ++ dapat membungkus integer ke kelas tanpa overhead apa pun (benchmark di semua tempat). Java harus memiliki metadata pointer-style + alignment padding overhead per objek yang karenanya Booleanlebih besar dari boolean(tetapi sebagai gantinya memberikan manfaat refleksi yang seragam dan kemampuan untuk mengesampingkan fungsi apa pun yang tidak ditandai finaluntuk setiap UDT tunggal).

Ini sedikit lebih mudah di C ++ untuk mengontrol kedekatan tata letak memori melintasi bidang non-homogen (mis: interleaving floats dan ints menjadi satu array melalui struct / kelas), karena lokalitas spasial sering hilang (atau setidaknya kontrol hilang) di Jawa saat mengalokasikan objek melalui GC.

... tetapi seringkali solusi berkinerja tinggi akan tetap memecahnya dan menggunakan pola akses SoA di atas susunan data lama yang berdekatan. Jadi untuk area yang membutuhkan kinerja puncak, strategi untuk mengoptimalkan tata letak memori antara Java dan C ++ seringkali sama, dan sering kali Anda akan menghancurkan antarmuka berorientasi objek kecil yang mendukung antarmuka gaya koleksi yang dapat melakukan hal-hal seperti hot / pemisahan medan dingin, repetisi SoA, dll. Repetisi AoSoA yang tidak homogen tampaknya tidak mungkin di Jawa (kecuali jika Anda hanya menggunakan array mentah byte atau sesuatu seperti itu), tetapi itu untuk kasus yang jarang terjadi di mana keduanyapola akses berurutan dan acak harus cepat sementara secara bersamaan memiliki campuran jenis bidang untuk bidang panas. Bagi saya sebagian besar perbedaan dalam strategi pengoptimalan (pada tingkat umum) antara keduanya diperdebatkan jika Anda ingin mencapai kinerja puncak.

Perbedaannya sedikit lebih bervariasi jika Anda hanya meraih kinerja "baik" - tidak ada yang bisa dilakukan dengan benda-benda kecil seperti Integervs intdapat menjadi sedikit lebih banyak dari PITA, terutama dengan cara berinteraksi dengan obat generik. . Agak sulit untuk hanya membangun satu struktur data generik sebagai target optimalisasi pusat di Jawa yang berfungsi untuk int,, floatdll. Sembari menghindari UDT yang lebih besar dan mahal, tetapi seringkali area yang paling kritis terhadap kinerja akan memerlukan pengguliran sendiri struktur data Anda sendiri disetel untuk tujuan yang sangat spesifik sehingga hanya mengganggu kode yang berusaha keras untuk kinerja yang baik tetapi tidak untuk kinerja puncak.

Overhead Objek

Perhatikan bahwa overhead objek Java (metadata dan kehilangan lokalitas spasial dan hilangnya temporalitas lokal temporer setelah siklus GC awal) sering besar untuk hal-hal yang sangat kecil (seperti intvs. Integer) yang disimpan oleh jutaan dalam beberapa struktur data yang sebagian besar berdekatan dan diakses dalam loop yang sangat ketat. Tampaknya ada banyak kepekaan tentang subjek ini, jadi saya harus mengklarifikasi bahwa Anda tidak ingin khawatir tentang overhead objek untuk objek besar seperti gambar, hanya objek yang sangat kecil seperti satu piksel.

Jika ada yang merasa ragu tentang bagian ini, saya sarankan membuat patokan antara menjumlahkan satu juta acak intsvs satu juta acak Integersdan untuk melakukan ini berulang kali ( Integerskehendak perombakan dalam memori setelah siklus GC awal).

Trik Ultimate: Desain Antarmuka Yang Memberikan Ruang untuk Optimalkan

Jadi trik Java utama seperti yang saya lihat jika Anda berhadapan dengan tempat yang menangani beban berat di atas benda-benda kecil (mis: a Pixel, 4-vektor, matriks 4x4, a Particle, mungkin bahkan Accountjika hanya memiliki beberapa kecil bidang) adalah untuk menghindari menggunakan objek untuk hal-hal kecil dan menggunakan array (mungkin dirantai bersama) dari data lama biasa. The benda kemudian menjadi interface koleksi seperti Image, ParticleSystem, Accounts, koleksi matriks atau vektor, dll yang Individu dapat diakses oleh indeks, misalnya Ini juga salah satu trik desain paling dalam C dan C ++, karena bahkan tanpa bahwa objek biaya overhead dasar dan memori terputus-putus, memodelkan antarmuka pada tingkat partikel tunggal mencegah solusi yang paling efisien.

ChrisF
sumber
1
Menimbang bahwa kinerja buruk dalam jumlah besar sebenarnya mungkin memiliki peluang yang layak untuk melampaui kinerja puncak di area kritis, saya tidak berpikir orang dapat sepenuhnya mengabaikan keuntungan memiliki kinerja yang baik dengan mudah. Dan trik mengubah array struct menjadi struct array agak rusak ketika semua (atau hampir semua) nilai yang terdiri dari salah satu struct asli akan diakses pada saat yang sama. BTW: Saya melihat Anda sedang menggali banyak posting lawas dan menambahkan jawaban baik Anda sendiri, kadang-kadang bahkan jawaban yang bagus ;-)
Deduplicator
1
@Deduplicator Berharap saya tidak mengganggu orang dengan menabrak terlalu banyak! Yang ini agak sedikit ranty - mungkin saya harus memperbaikinya sedikit. SoA vs AoS sering sulit bagi saya (akses berurutan vs acak). Saya jarang tahu di muka mana yang harus saya gunakan karena sering ada campuran akses berurutan dan acak dalam kasus saya. Pelajaran berharga yang sering saya pelajari adalah merancang antarmuka yang menyisakan cukup ruang untuk bermain dengan representasi data - antarmuka yang lebih besar yang memiliki algoritma transformasi besar bila memungkinkan (kadang-kadang tidak mungkin dengan bit kecil yang diakses secara acak di sana-sini).
1
Yah, saya hanya memperhatikan karena semuanya sangat lambat. Dan saya mengambil waktu saya dengan masing-masing.
Deduplicator
Saya benar-benar bertanya-tanya mengapa user204677pergi. Jawaban yang sangat bagus.
oligofren
3

Ada area tengah antara optimasi mikro, di satu sisi, dan pilihan algoritma yang baik, di sisi lain.

Ini adalah area percepatan faktor-konstan, dan dapat menghasilkan pesanan besar.
Cara melakukannya adalah dengan memotong seluruh fraksi waktu eksekusi, seperti 30% pertama, lalu 20% dari yang tersisa, lalu 50% dari itu, dan seterusnya untuk beberapa iterasi, sampai hampir tidak ada yang tersisa.

Anda tidak melihat ini dalam program bergaya demo kecil. Di mana Anda melihatnya ada dalam program serius besar dengan banyak struktur data kelas, di mana tumpukan panggilan biasanya memiliki banyak lapisan. Cara yang baik untuk menemukan peluang percepatan adalah dengan memeriksa sampel waktu-acak dari status program.

Secara umum, speedup terdiri dari hal-hal seperti:

  • meminimalkan panggilan newdengan menggabungkan dan menggunakan kembali benda-benda tua,

  • mengenali hal-hal yang dilakukan yang ada di sana untuk kepentingan umum, daripada benar-benar diperlukan,

  • merevisi struktur data dengan menggunakan kelas koleksi yang berbeda yang memiliki perilaku big-O yang sama tetapi mengambil keuntungan dari pola akses yang sebenarnya digunakan,

  • menyimpan data yang diperoleh dengan pemanggilan fungsi alih-alih memanggil kembali fungsi, (Ini adalah kecenderungan alami dan lucu dari pemrogram untuk menganggap bahwa fungsi yang memiliki nama pendek dieksekusi lebih cepat.)

  • mentolerir sejumlah ketidakkonsistenan antara struktur data yang berlebihan, yang bertentangan dengan upaya untuk menjaga mereka sepenuhnya konsisten dengan acara pemberitahuan,

  • dll. dll

Tetapi tentu saja tidak satu pun dari hal-hal ini harus dilakukan tanpa terlebih dahulu terbukti menjadi masalah dengan mengambil sampel.

Mike Dunlavey
sumber
2

Java (sejauh yang saya ketahui) memberi Anda tidak ada kontrol atas lokasi variabel dalam memori sehingga Anda memiliki waktu lebih sulit untuk menghindari hal-hal seperti pembagian yang salah dan penjajaran variabel (Anda bisa keluar kelas dengan beberapa anggota yang tidak digunakan). Hal lain yang saya pikir Anda tidak bisa manfaatkan adalah instruksi seperti mmpause, tetapi hal-hal ini adalah CPU khusus dan jadi jika Anda pikir Anda membutuhkannya Java mungkin bukan bahasa yang digunakan.

Ada kelas Tidak Aman yang memberi Anda fleksibilitas C / C ++ tetapi juga dengan bahaya C / C ++.

Mungkin membantu Anda melihat kode assembly yang dihasilkan JVM untuk kode Anda

Untuk membaca tentang aplikasi Java yang melihat detail semacam ini, lihat kode Disruptor yang dirilis oleh LMAX

James
sumber
2

Pertanyaan ini sangat sulit dijawab, karena tergantung pada implementasi bahasa.

Secara umum ada sangat sedikit ruang untuk "optimasi mikro" seperti ini. Alasan utama adalah bahwa kompiler mengambil keuntungan dari optimasi tersebut selama kompilasi. Misalnya tidak ada perbedaan kinerja antara operator pra-kenaikan dan pasca kenaikan dalam situasi di mana semantiknya identik. Contoh lain akan misalnya loop seperti ini di for(int i=0; i<vec.size(); i++)mana orang bisa berpendapat bahwa alih-alih memanggilsize()fungsi anggota selama setiap iterasi akan lebih baik untuk mendapatkan ukuran vektor sebelum loop dan kemudian membandingkan terhadap variabel tunggal dan dengan demikian menghindari fungsi panggilan per iterasi. Namun, ada kasus di mana kompiler akan mendeteksi kasus konyol ini dan menyimpan hasilnya. Namun, ini hanya dimungkinkan ketika fungsi tidak memiliki efek samping dan kompiler dapat memastikan bahwa ukuran vektor tetap konstan selama loop sehingga hanya berlaku untuk kasus yang cukup sepele.

zxcdw
sumber
Adapun kasus kedua, saya tidak berpikir kompiler dapat mengoptimalkannya di masa mendatang. Mendeteksi bahwa aman untuk mengoptimalkan vec.size () tergantung pada pembuktian bahwa ukuran jika vektor / hilang tidak berubah di dalam loop, yang saya percaya tidak dapat dipastikan karena masalah penghentian.
Lie Ryan
@ LieRyan Saya telah melihat banyak kasus (sederhana) di mana kompiler telah menghasilkan file biner yang persis sama jika hasilnya telah secara manual "di-cache" dan jika ukuran () telah dipanggil. Saya menulis beberapa kode dan ternyata perilakunya sangat tergantung pada cara program beroperasi. Ada beberapa kasus di mana kompiler dapat menjamin bahwa tidak ada kemungkinan untuk ukuran vektor berubah selama loop, dan kemudian ada kasus di mana kompiler tidak dapat menjaminnya, sangat mirip dengan masalah penghentian seperti yang Anda sebutkan. Untuk saat ini saya tidak dapat memverifikasi klaim saya (C ++ pembongkaran itu
menyusahkan
2
@Lie Ryan: banyak hal yang tidak dapat diputuskan dalam kasus umum sangat cocok untuk kasus-kasus khusus tetapi umum, dan hanya itu yang Anda butuhkan di sini.
Michael Borgwardt
@ LieRyan Jika Anda hanya memanggil constmetode pada vektor ini, saya cukup yakin banyak kompiler yang mengoptimalkan akan mengetahuinya.
K.Steff
di C #, dan saya pikir saya membaca di Jawa juga, jika Anda tidak ukuran cache kompiler tahu itu dapat menghapus cek untuk melihat apakah Anda pergi di luar batas array, dan jika Anda melakukan ukuran cache itu harus melakukan pemeriksaan , yang biasanya lebih mahal daripada yang Anda hemat dengan caching. Mencoba mengakali pengoptimal jarang merupakan rencana yang bagus.
Kate Gregory
1

bisa orang memberi contoh trik apa yang dapat Anda gunakan di Jawa (selain flag kompiler sederhana).

Selain peningkatan algoritma, pastikan untuk mempertimbangkan hirarki memori dan bagaimana prosesor memanfaatkannya. Ada manfaat besar dalam mengurangi latensi akses memori, setelah Anda memahami bagaimana bahasa tersebut mengalokasikan memori ke tipe data dan objeknya.

Contoh Java untuk mengakses array int 1000x1000

Pertimbangkan kode contoh di bawah ini - ini mengakses area memori yang sama (array int 1000x1000), tetapi dalam urutan yang berbeda. Pada mac mini saya (Core i7, 2,7 GHz) hasilnya adalah sebagai berikut, menunjukkan bahwa melintasi array dengan baris lebih dari dua kali lipat kinerja (rata-rata lebih dari 100 putaran masing-masing).

Processing columns by rows*** took 4 ms (avg)
Processing rows by columns*** took 10 ms (avg) 

Ini karena array disimpan sedemikian rupa sehingga kolom berturut-turut (yaitu nilai int) ditempatkan berdekatan dalam memori, sedangkan baris berturut-turut tidak. Agar prosesor benar-benar menggunakan data, ia harus ditransfer ke cache. Transfer memori oleh blok byte, yang disebut garis cache - memuat garis cache langsung dari memori memperkenalkan latensi dan dengan demikian mengurangi kinerja program.

Untuk Core i7 (sandy bridge) garis cache menampung 64 byte, sehingga setiap akses memori mengambil 64 byte. Karena tes pertama mengakses memori dalam urutan yang dapat diprediksi, prosesor akan melakukan pra-pengambilan data sebelum benar-benar dikonsumsi oleh program. Secara keseluruhan, ini menghasilkan lebih sedikit latensi pada akses memori dan dengan demikian meningkatkan kinerja.

Kode sampel:

  package test;

  import java.lang.*;

  public class PerfTest {
    public static void main(String[] args) {
      int[][] numbers = new int[1000][1000];
      long startTime;
      long stopTime;
      long elapsedAvg;
      int tries;
      int maxTries = 100;

      // process columns by rows 
      System.out.print("Processing columns by rows");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int r = 0; r < 1000; r++) {
         for(int c = 0; c < 1000; c++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     

      // process rows by columns
      System.out.print("Processing rows by columns");
      for(tries = 0, elapsedAvg = 0; tries < maxTries; tries++) {
       startTime = System.currentTimeMillis();
       for(int c = 0; c < 1000; c++) {
         for(int r = 0; r < 1000; r++) {
           int v = numbers[r][c]; 
         }
       }
       stopTime = System.currentTimeMillis();
       elapsedAvg += ((stopTime - startTime) - elapsedAvg) / (tries + 1);
      }

      System.out.format("*** took %d ms (avg)\n", elapsedAvg);     
    }
  }
miraculixx
sumber
1

JVM dapat dan sering mengganggu, dan kompiler JIT dapat berubah secara signifikan antar versi. Beberapa optimasi mikro tidak dimungkinkan di Jawa karena keterbatasan bahasa, seperti menjadi hyper-threading friendly atau koleksi SIMD prosesor Intel terbaru.

Sebuah blog yang sangat informatif tentang topik dari salah satu penulis Disruptor disarankan dibaca:

Kita selalu harus bertanya mengapa repot menggunakan Java jika Anda ingin optimasi mikro, ada banyak metode alternatif untuk percepatan fungsi seperti menggunakan JNA atau JNI untuk meneruskan ke perpustakaan asli.

Steve-o
sumber