Apakah Anda merasa ada pertukaran antara menulis kode berorientasi objek "bagus" dan menulis kode latensi rendah yang sangat cepat? Misalnya, menghindari fungsi virtual dalam C ++ / overhead polimorfisme dll- penulisan ulang kode yang terlihat buruk, tetapi apakah sangat cepat, dll?
Saya bekerja di bidang yang sedikit lebih fokus pada throughput daripada latensi, tetapi sangat kritis terhadap kinerja, dan saya akan mengatakan "agak" .
Namun masalahnya adalah bahwa begitu banyak orang yang salah memahami kinerja mereka. Pemula sering mendapatkan semua yang salah, dan seluruh model konseptual mereka "biaya komputasi" perlu pengerjaan ulang, dengan hanya kompleksitas algoritme yang menjadi satu-satunya hal yang dapat mereka perbaiki. Perantara mendapatkan banyak hal yang salah. Para ahli salah.
Mengukur dengan alat yang akurat yang dapat memberikan metrik seperti cache yang hilang dan salah duga cabang adalah hal yang membuat semua orang dari semua tingkat keahlian di bidang ini dalam kendali.
Mengukur juga menunjukkan apa yang tidak optimal . Para ahli sering menghabiskan lebih sedikit waktu untuk mengoptimalkan daripada pemula, karena mereka mengoptimalkan hotspot yang benar diukur dan tidak mencoba untuk mengoptimalkan tusukan liar dalam gelap berdasarkan firasat tentang apa yang bisa lambat (yang, dalam bentuk ekstrim, dapat menggoda seseorang untuk mengoptimalkan mikro hanya tentang setiap baris lain dalam basis kode).
Merancang untuk Kinerja
Selain itu, kunci untuk mendesain kinerja berasal dari bagian desain , seperti dalam desain antarmuka. Salah satu masalah dengan pengalaman kurang adalah bahwa cenderung ada perubahan awal pada metrik implementasi absolut, seperti biaya panggilan fungsi tidak langsung dalam beberapa konteks umum, seolah-olah biaya (yang lebih baik dipahami dalam arti langsung dari titik pengoptimal) pandangan daripada sudut pandang percabangan) adalah alasan untuk menghindarinya di seluruh basis kode.
Biaya relatif . Meskipun ada biaya untuk pemanggilan fungsi tidak langsung, misalnya, semua biaya relatif. Jika Anda membayar biaya satu kali untuk memanggil fungsi yang melewati jutaan elemen, mengkhawatirkan biaya ini seperti menghabiskan berjam-jam menawar uang untuk membeli produk miliar dolar, hanya untuk menyimpulkan tidak membeli produk itu karena satu sen terlalu mahal.
Desain Antarmuka Lebih Kasar
Aspek desain antarmuka kinerja sering berusaha sebelumnya untuk mendorong biaya-biaya ini ke tingkat yang lebih kasar. Alih-alih membayar biaya abstraksi runtime untuk satu partikel, misalnya, kita mungkin mendorong biaya itu ke tingkat sistem partikel / emitor, secara efektif menjadikan partikel ke dalam detail implementasi dan / atau hanya data mentah dari pengumpulan partikel ini.
Jadi desain berorientasi objek tidak harus tidak kompatibel dengan mendesain untuk kinerja (apakah latensi atau throughput), tetapi mungkin ada godaan dalam bahasa yang berfokus padanya untuk memodelkan objek granular yang semakin kecil, dan di sana pengoptimal terbaru tidak dapat membantu. Itu tidak dapat melakukan hal-hal seperti menyatukan kelas yang mewakili satu titik dengan cara yang menghasilkan representasi SoA yang efisien untuk pola akses memori perangkat lunak. Kumpulan poin dengan desain antarmuka yang dimodelkan pada tingkat kekasaran menawarkan peluang itu, dan memungkinkan iterasi ke arah solusi yang lebih dan lebih optimal sesuai kebutuhan. Desain seperti ini dirancang untuk memori massal *.
* Catat fokus pada memori di sini dan bukan data , karena bekerja di area yang sangat kritis untuk waktu yang lama akan cenderung mengubah pandangan Anda tentang tipe data dan struktur data dan melihat bagaimana mereka terhubung ke memori. Pohon pencarian biner tidak lagi menjadi semata-mata tentang kompleksitas logaritmik dalam kasus-kasus seperti potongan memori yang mungkin berbeda dan tidak ramah cache untuk simpul pohon kecuali dibantu oleh pengalokasi tetap. Tampilan tidak mengabaikan kompleksitas algoritmik, tetapi melihatnya tidak lagi terlepas dari tata letak memori. Kita juga mulai melihat iterasi pekerjaan sebagai lebih banyak tentang iterasi akses memori. *
Banyak desain yang sangat penting untuk kinerja sebenarnya sangat kompatibel dengan konsep desain antarmuka tingkat tinggi yang mudah dipahami dan digunakan manusia. Perbedaannya adalah bahwa "level tinggi" dalam konteks ini adalah tentang agregasi massal memori, sebuah antarmuka yang dimodelkan untuk koleksi data yang berpotensi besar, dan dengan implementasi di bawah kap yang mungkin levelnya cukup rendah. Analogi visual mungkin adalah mobil yang benar-benar nyaman dan mudah dikendarai dan dipegang serta sangat aman saat berjalan dengan kecepatan suara, tetapi jika Anda membuka penutupnya, ada sedikit setan api yang bernapas di dalam.
Dengan desain yang lebih kasar juga cenderung menjadi cara yang lebih mudah untuk memberikan pola penguncian yang lebih efisien dan mengeksploitasi paralelisme dalam kode (multithreading adalah subjek lengkap yang saya akan lewati di sini).
Memory Pool
Aspek kritis dari pemrograman latensi rendah mungkin akan menjadi kontrol yang sangat eksplisit atas memori untuk meningkatkan lokalitas referensi serta hanya kecepatan umum mengalokasikan dan membatalkan alokasi memori. Memori pengalokasi pengalokasi khusus sebenarnya menggaungkan jenis pola pikir desain yang sama seperti yang kami gambarkan. Ini dirancang untuk massal ; itu dirancang pada tingkat kasar. Ini mengalokasikan memori dalam blok besar dan menyatukan memori yang sudah dialokasikan dalam potongan kecil.
Idenya persis sama dengan mendorong hal-hal yang mahal (mengalokasikan potongan memori terhadap pengalokasi tujuan umum, misalnya) ke tingkat yang lebih kasar dan lebih kasar. Kumpulan memori dirancang untuk menangani memori secara massal .
Jenis Sistem Pisahkan Memori
Salah satu kesulitan dengan desain berorientasi objek granular dalam bahasa apa pun adalah bahwa ia sering ingin memperkenalkan banyak tipe dan struktur data yang didefinisikan pengguna. Jenis-jenis itu kemudian dapat dialokasikan dalam potongan kecil jika dialokasikan secara dinamis.
Contoh umum dalam C ++ adalah untuk kasus-kasus di mana polimorfisme diperlukan, di mana godaan alami adalah untuk mengalokasikan setiap instance dari subclass terhadap pengalokasi memori tujuan umum.
Ini akhirnya memecah-mecah tata letak memori yang mungkin bersebelahan menjadi sedikit demi sedikit bit-bit yang tersebar di seluruh rentang pengalamatan yang diterjemahkan menjadi lebih banyak kesalahan halaman dan cache misses.
Bidang-bidang yang menuntut respons laten terendah, bebas gagap, deterministik mungkin adalah satu-satunya tempat di mana hotspot tidak selalu berubah menjadi hambatan tunggal, di mana inefisiensi kecil sebenarnya dapat benar-benar semacam "terakumulasi" (sesuatu yang banyak orang bayangkan terjadi secara tidak benar dengan profiler untuk memastikannya, tetapi di bidang latensi yang digerakkan, sebenarnya bisa ada beberapa kasus yang jarang terjadi di mana akumulasi ketidakefisienan kecil). Dan banyak alasan paling umum untuk akumulasi seperti ini bisa jadi ini: alokasi yang berlebihan dari potongan memori kecil di semua tempat.
Dalam bahasa seperti Java, akan sangat membantu untuk menggunakan lebih banyak array tipe data lama polos bila memungkinkan untuk area bottlenecky (area yang diproses dalam loop ketat) seperti array int
(tetapi masih di belakang antarmuka tingkat tinggi yang besar) alih-alih, katakanlah , sebuah objek yang ArrayList
ditentukan pengguna Integer
. Ini menghindari pemisahan memori yang biasanya menyertai yang terakhir. Dalam C ++, kita tidak harus menurunkan struktur sebanyak jika pola alokasi memori kita efisien, karena tipe yang ditentukan pengguna dapat dialokasikan secara berdekatan di sana dan bahkan dalam konteks wadah generik.
Memori Sekering Kembali Bersama
Solusi di sini adalah menjangkau pengalokasi khusus untuk tipe data yang homogen, dan mungkin bahkan lintas tipe data yang homogen. Ketika tipe data kecil dan struktur data diratakan menjadi bit dan byte dalam memori, mereka mengambil sifat yang homogen (meskipun dengan beberapa persyaratan keberpihakan yang berbeda). Ketika kita tidak melihat mereka dari pola pikir sentris-memori, jenis sistem bahasa pemrograman "ingin" untuk membagi / memisahkan wilayah memori yang berpotensi bersebelahan menjadi beberapa potongan kecil yang tersebar.
Tumpukan menggunakan fokus sentris-memori ini untuk menghindari hal ini dan berpotensi menyimpan kombinasi campuran yang mungkin dari instance tipe yang ditentukan pengguna di dalamnya. Memanfaatkan tumpukan lebih banyak adalah ide bagus bila memungkinkan karena bagian atasnya hampir selalu duduk di baris cache, tetapi kita juga dapat merancang pengalokasi memori yang meniru beberapa karakteristik ini tanpa pola LIFO, menggabungkan memori melintasi tipe data yang berbeda menjadi berdekatan. potongan bahkan untuk alokasi memori dan pola alokasi yang lebih kompleks.
Perangkat keras modern dirancang untuk mencapai puncaknya ketika memproses blok memori yang berdekatan (berulang kali mengakses jalur cache yang sama, halaman yang sama, misalnya). Kata kunci ada persentuhan, karena ini hanya menguntungkan jika ada data yang menarik di sekitarnya. Jadi banyak kunci (namun juga kesulitan) untuk kinerja adalah memadukan potongan-potongan memori yang terpisah kembali bersama-sama menjadi blok-blok yang berdekatan yang diakses secara keseluruhan (semua data di sekitarnya menjadi relevan) sebelum penggusuran. Sistem tipe kaya terutama tipe yang ditentukan pengguna dalam bahasa pemrograman dapat menjadi kendala terbesar di sini, tetapi kami selalu dapat menjangkau dan menyelesaikan masalah melalui pengalokasi khusus dan / atau desain bulkier bila sesuai.
Jelek
"Jelek" sulit dikatakan. Ini adalah metrik subjektif, dan seseorang yang bekerja di bidang yang sangat kritis terhadap kinerja akan mulai mengubah gagasan mereka tentang "keindahan" menjadi yang lebih berorientasi pada data dan berfokus pada antarmuka yang memproses berbagai hal secara massal.
Berbahaya
"Berbahaya" mungkin lebih mudah. Secara umum, kinerja cenderung ingin mencapai kode tingkat yang lebih rendah. Mengimplementasikan pengalokasi memori, misalnya, tidak mungkin tanpa mencapai di bawah tipe data dan bekerja pada level berbahaya bit dan byte mentah. Sebagai hasilnya, ini dapat membantu meningkatkan fokus pada prosedur pengujian yang cermat dalam subsistem yang sangat penting ini, meningkatkan ketelitian pengujian dengan tingkat optimisasi yang diterapkan.
Keindahan
Namun semua ini akan berada pada level detail implementasi. Baik dalam skala besar veteran maupun pola pikir kritis-kinerja, "keindahan" cenderung beralih ke desain antarmuka daripada detail implementasi. Ini menjadi prioritas yang secara eksponensial lebih tinggi untuk mencari antarmuka yang "indah", dapat digunakan, aman, efisien daripada implementasi karena kerusakan kopling dan kaskade yang dapat terjadi dalam menghadapi perubahan desain antarmuka. Implementasi dapat ditukar kapan saja. Kami biasanya beralih ke kinerja sesuai kebutuhan, dan sebagaimana ditunjukkan oleh pengukuran. Kunci dengan desain antarmuka adalah untuk memodelkan pada tingkat yang cukup kasar untuk meninggalkan ruang untuk iterasi seperti itu tanpa merusak seluruh sistem.
Bahkan, saya akan menyarankan bahwa fokus veteran pada pengembangan kinerja-kritis sering cenderung menempatkan fokus utama pada keselamatan, pengujian, perawatan, hanya murid SE secara umum, karena basis kode skala besar yang memiliki sejumlah kinerja subsistem-kritis (sistem partikel, algoritma pemrosesan gambar, pemrosesan video, umpan balik audio, raytracer, mesin mesh, dll) perlu memperhatikan teknik perangkat lunak untuk menghindari tenggelam dalam mimpi buruk pemeliharaan. Bukan kebetulan bahwa seringkali produk yang paling efisien di luar sana juga dapat memiliki jumlah bug paling sedikit.
TL; DR
Pokoknya, itulah pendapat saya tentang masalah ini, mulai dari prioritas di bidang yang benar-benar kritis terhadap kinerja, apa yang dapat mengurangi latensi dan menyebabkan inefisiensi kecil menumpuk, dan apa yang sebenarnya merupakan "keindahan" (ketika melihat hal-hal yang paling produktif).