[...] (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 Pixel
objek (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 Pixel
objek, menggunakan array byte, dan memodelkan Image
objek. 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 int
alih-alih Integer
) dan mulai memodelkan antarmuka massal seperti Image
antarmuka, bukan Pixel
antarmuka. 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 int
jaminan representasi yang berdekatan. Array Integer
tidak. 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 Integer
mungkin 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 biasaIntegers
baik-baik saja di samping satu sama lain dalam memori, kita hanya bisa memasukkan 4 ke dalam garis cache yang dapat diakses sebelum penggusuran karena Integer
menjadi 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 Boolean
lebih besar dari boolean
(tetapi sebagai gantinya memberikan manfaat refleksi yang seragam dan kemampuan untuk mengesampingkan fungsi apa pun yang tidak ditandai final
untuk 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 Integer
vs int
dapat 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
,, float
dll. 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 int
vs. 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 ints
vs satu juta acak Integers
dan untuk melakukan ini berulang kali ( Integers
kehendak 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 Account
jika 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.