Orientasi objek telah banyak membantu saya dalam mengimplementasikan banyak algoritma. Namun, bahasa berorientasi objek terkadang memandu Anda dalam pendekatan "langsung" dan saya ragu apakah pendekatan ini selalu merupakan hal yang baik.
OO sangat membantu dalam pengkodean algoritma dengan cepat dan mudah. Tetapi mungkinkah OOP ini menjadi kerugian bagi perangkat lunak berdasarkan kinerja, yaitu seberapa cepat program dijalankan?
Misalnya, menyimpan node grafik dalam struktur data tampaknya "langsung" pada awalnya, tetapi jika objek Node mengandung banyak atribut dan metode, dapatkah ini mengarah pada algoritma yang lambat?
Dengan kata lain, dapatkah banyak referensi antara banyak objek yang berbeda, atau menggunakan banyak metode dari banyak kelas, menghasilkan implementasi yang "berat"?
sumber
Jawaban:
Orientasi objek dapat mencegah optimasi algoritmik tertentu, karena enkapsulasi. Dua algoritma mungkin bekerja sangat baik bersama-sama, tetapi jika mereka tersembunyi di balik antarmuka OO, kemungkinan untuk menggunakan sinergi mereka hilang.
Lihatlah perpustakaan numerik. Banyak dari mereka (tidak hanya yang ditulis pada tahun 60an atau 70an) bukan OOP. Ada alasan untuk itu - algoritma numerik bekerja lebih baik sebagai seperangkat dipisahkan
modules
daripada sebagai hierarki OO dengan antarmuka dan enkapsulasi.sumber
Apa yang menentukan kinerja?
Fundamental: struktur data, algoritma, arsitektur komputer, perangkat keras. Ditambah overhead.
Program OOP dapat dirancang untuk menyelaraskan dengan tepat dengan pilihan struktur data dan algoritma yang dianggap optimal oleh teori CS. Ini akan memiliki karakteristik kinerja yang sama dengan program optimal, ditambah beberapa overhead. Overhead biasanya dapat diminimalkan.
Namun, sebuah program yang awalnya dirancang hanya dengan keprihatinan OOP, tanpa memperhatikan dasar-dasarnya, mungkin awalnya kurang optimal. Sub-optimalitas kadang-kadang dapat dilepas dengan refactoring; kadang-kadang tidak - membutuhkan penulisan ulang yang lengkap.
Peringatan: apakah kinerja penting dalam perangkat lunak bisnis?
Ya, tetapi time-to-market (TTM) lebih penting, berdasarkan pesanan besarnya. Perangkat lunak bisnis menekankan pada kemampuan adaptasi kode pada aturan bisnis yang kompleks. Pengukuran kinerja harus dilakukan sepanjang siklus hidup pengembangan. (Lihat bagian: apa arti kinerja optimal? ) Hanya perangkat tambahan yang dapat dipasarkan harus dibuat, dan harus secara bertahap diperkenalkan di versi yang lebih baru.
Apa arti kinerja optimal?
Secara umum, masalah dengan kinerja perangkat lunak adalah: untuk membuktikan bahwa "versi yang lebih cepat ada", versi yang lebih cepat harus ada terlebih dahulu (yaitu tidak ada bukti selain dari dirinya sendiri).
Terkadang versi yang lebih cepat pertama kali dilihat dalam bahasa atau paradigma yang berbeda. Ini harus diambil sebagai petunjuk untuk perbaikan, bukan penilaian inferioritas dari beberapa bahasa atau paradigma lain.
Mengapa kami melakukan OOP jika hal itu dapat menghambat pencarian kami untuk kinerja yang optimal?
OOP memperkenalkan overhead (dalam ruang dan eksekusi), sebagai imbalan untuk meningkatkan "kemampuan kerja" dan karenanya nilai bisnis dari kode. Ini mengurangi biaya pengembangan lebih lanjut dan optimalisasi. Lihat @MikeNakis .
Bagian mana dari OOP yang dapat mendorong desain yang awalnya kurang optimal?
Bagian-bagian OOP yang (i) mendorong kesederhanaan / intuisi, (ii) penggunaan metode desain sehari-hari alih-alih fundamental, (iii) mencegah beberapa implementasi yang dirancang khusus untuk tujuan yang sama.
Penerapan ketat beberapa pedoman OOP (enkapsulasi, pengiriman pesan, melakukan satu hal dengan baik) memang akan menghasilkan kode lebih lambat pada awalnya. Pengukuran kinerja akan membantu mendiagnosis masalah-masalah tersebut. Selama struktur data dan algoritma sejalan dengan desain optimal yang diprediksi oleh teori, overhead biasanya dapat diminimalkan.
Apa mitigasi umum untuk overhead OOP?
Seperti yang dinyatakan sebelumnya, menggunakan struktur data yang optimal untuk desain.
Beberapa bahasa mendukung inlining kode yang dapat memulihkan beberapa kinerja runtime.
Bagaimana kita bisa mengadopsi OOP tanpa mengorbankan kinerja?
Pelajari dan terapkan OOP dan fundamentalnya.
Memang benar bahwa kepatuhan yang ketat terhadap OOP dapat mencegah Anda menulis versi yang lebih cepat. Terkadang versi yang lebih cepat hanya dapat ditulis dari awal. Inilah sebabnya mengapa membantu untuk menulis beberapa versi kode menggunakan algoritma dan paradigma yang berbeda (OOP, generik, fungsional, matematis, spageti), dan kemudian menggunakan alat optimisasi untuk membuat setiap versi mendekati kinerja maksimal yang diamati.
Apakah ada jenis kode yang tidak akan mendapat manfaat dari OOP?
(Diperluas dari diskusi antara [@quant_dev], [@ SK-logic] dan [@MikeNakis])
*
sumber
Itu tidak benar-benar tentang orientasi objek, tetapi tentang wadah. Jika Anda menggunakan daftar ditautkan ganda untuk menyimpan piksel dalam pemutar video Anda, itu akan menderita.
Namun jika Anda menggunakan wadah yang benar, tidak ada alasan std :: vector lebih lambat daripada array, dan karena Anda memiliki semua algoritma umum yang telah ditulis untuk itu - oleh para ahli - mungkin lebih cepat daripada kode array digulung di rumah Anda.
sumber
OOP jelas merupakan ide yang bagus, dan seperti halnya ide bagus, itu bisa digunakan secara berlebihan. Dalam pengalaman saya itu terlalu banyak digunakan. Kinerja buruk dan hasil pemeliharaan yang buruk.
Ini tidak ada hubungannya dengan overhead memanggil fungsi virtual, dan tidak banyak hubungannya dengan apa yang dilakukan optimizer / jitter.
Ini semua berhubungan dengan struktur data yang, walaupun memiliki kinerja big-O terbaik, memiliki faktor konstan yang sangat buruk. Ini dilakukan dengan asumsi bahwa jika ada masalah yang membatasi kinerja dalam aplikasi, itu ada di tempat lain.
Salah satu cara manifes ini adalah berapa kali per detik baru dilakukan, yang dianggap memiliki kinerja O (1), tetapi dapat menjalankan ratusan hingga ribuan instruksi (termasuk penghapusan yang cocok atau waktu GC). Itu dapat dikurangi dengan menyimpan objek yang digunakan, tetapi itu membuat kode kurang "bersih".
Cara lain yang dimanifestasikan adalah cara orang didorong untuk menulis fungsi properti, penangan notifikasi, panggilan ke fungsi kelas dasar, semua jenis panggilan fungsi bawah tanah yang ada untuk mencoba mempertahankan konsistensi. Untuk mempertahankan konsistensi, keberhasilan mereka terbatas, tetapi mereka sangat sukses dalam membuang-buang siklus. Pemrogram memahami konsep data yang dinormalisasi tetapi mereka cenderung menerapkannya hanya untuk desain database. Mereka tidak menerapkannya pada desain struktur data, setidaknya sebagian karena OOP memberi tahu mereka bahwa mereka tidak perlu melakukannya. Sesederhana mengatur bit yang Dimodifikasi dalam suatu objek dapat mengakibatkan tsunami pembaruan yang berjalan melalui struktur data, karena tidak ada kelas yang layak untuk dikodekan oleh panggilan yang dimodifikasi dan hanya menyimpannya .
Mungkin kinerja aplikasi yang diberikan baik-baik saja seperti yang tertulis.
Di sisi lain, jika ada masalah kinerja, berikut adalah contoh bagaimana saya menyetelnya. Ini adalah proses multi-tahap. Pada setiap tahap, beberapa aktivitas tertentu menyumbang sebagian besar waktu dan dapat digantikan oleh sesuatu yang lebih cepat. (Saya tidak mengatakan "bottleneck". Ini bukan hal-hal yang dapat ditemukan oleh para pembuat profil.) Proses ini sering kali mengharuskan, untuk mendapatkan penggantian cepat, penggantian struktur data grosir. Seringkali struktur data itu ada hanya karena itu direkomendasikan praktik OOP.
sumber
Secara teori, ini bisa menyebabkan kelambatan, tetapi meskipun begitu, itu bukan algoritma yang lambat, itu akan menjadi implementasi yang lambat. Dalam praktiknya, orientasi objek akan memungkinkan Anda untuk mencoba berbagai skenario bagaimana-jika (atau meninjau kembali algoritma di masa depan) dan dengan demikian memberikan peningkatan algoritmik untuk itu, yang Anda tidak akan pernah bisa capai jika Anda telah menuliskannya dengan cara spageti di awal tempat, karena tugas itu akan menakutkan. (Anda pada dasarnya harus menulis ulang semuanya.)
Misalnya, dengan membagi berbagai tugas dan entitas ke objek yang terpotong bersih, Anda mungkin dapat dengan mudah masuk nanti dan, misalnya, menyematkan fasilitas penyimpanan di antara beberapa objek, (transparan untuknya,) yang dapat menghasilkan ribuan- perbaikan lipat.
Secara umum, jenis perbaikan yang dapat Anda capai dengan menggunakan bahasa tingkat rendah (atau trik pintar dengan bahasa tingkat tinggi) memberikan peningkatan waktu (linier) konstan, yang tidak memperhitungkan notasi oh besar. Dengan peningkatan algoritmik Anda mungkin dapat mencapai peningkatan non-linear. Itu sangat berharga.
sumber
Sering ya !!! TAPI...
Belum tentu. Ini tergantung pada bahasa / kompiler. Misalnya, kompiler C ++ yang mengoptimalkan, asalkan Anda tidak menggunakan fungsi virtual, sering akan menurunkan overhead objek Anda menjadi nol. Anda dapat melakukan hal-hal seperti menulis pembungkus di
int
sana atau pointer cerdas scoping atas pointer lama polos yang melakukan secepat menggunakan tipe data lama polos ini secara langsung.Dalam bahasa lain seperti Jawa, ada sedikit overhead pada suatu objek (seringkali cukup kecil dalam banyak kasus, tetapi astronomi dalam beberapa kasus langka dengan objek yang sangat kecil). Sebagai contoh,
Integer
ada jauh lebih efisien daripadaint
(mengambil 16 byte dibandingkan dengan 4 pada 64-bit). Namun ini bukan hanya limbah terang-terangan atau semacamnya. Sebagai gantinya, Java menawarkan hal-hal seperti refleksi pada setiap jenis tunggal yang ditetapkan pengguna secara seragam, serta kemampuan untuk menimpa fungsi apa pun yang tidak ditandaifinal
.Namun mari kita ambil skenario kasus terbaik: kompiler C ++ yang mengoptimalkan yang dapat mengoptimalkan antarmuka objek hingga nol overhead. Meski begitu, OOP akan sering menurunkan kinerja dan mencegahnya mencapai puncak. Itu mungkin terdengar seperti paradoks lengkap: bagaimana mungkin? Masalahnya terletak pada:
Desain dan Enkapsulasi Antarmuka
Masalahnya adalah bahwa bahkan ketika kompiler dapat menekan struktur objek hingga nol overhead (yang setidaknya sangat sering benar untuk mengoptimalkan kompiler C ++), enkapsulasi dan desain antarmuka (dan dependensi terakumulasi) dari objek berbutir halus akan sering mencegah sebagian besar representasi data optimal untuk objek yang dimaksudkan untuk dikumpulkan oleh massa (yang sering terjadi pada perangkat lunak yang sangat kritis terhadap kinerja).
Ambil contoh ini:
Katakanlah pola akses memori kita adalah dengan hanya melalui partikel-partikel ini secara berurutan dan memindahkannya di sekitar setiap frame berulang kali, memantulkannya dari sudut layar dan kemudian memberikan hasilnya.
Kita sudah dapat melihat overhead padding 4 byte yang mencolok diperlukan untuk menyelaraskan
birth
anggota dengan benar ketika partikel-partikel dikumpulkan secara berdekatan. Sudah ~ 16,7% dari memori terbuang sia-sia dengan ruang yang digunakan untuk penyelarasan.Ini mungkin tampak diperdebatkan karena kami memiliki gigabyte DRAM hari ini. Namun, bahkan mesin paling kejam yang kita miliki saat ini sering hanya memiliki hanya 8 megabyte ketika datang ke wilayah cache CPU yang paling lambat dan terbesar (L3). Semakin sedikit yang dapat kami muat di sana, semakin banyak kami membayar untuk itu dalam hal akses DRAM berulang, dan semakin lambat hasilnya. Tiba-tiba, membuang 16,7% dari memori tidak lagi seperti kesepakatan sepele.
Kami dapat dengan mudah menghilangkan overhead ini tanpa berdampak pada penyelarasan lapangan:
Sekarang kami telah mengurangi memori dari 24 MB menjadi 20 MB. Dengan pola akses berurutan, mesin sekarang akan mengkonsumsi data ini sedikit lebih cepat.
Tapi mari kita lihat
birth
bidang ini sedikit lebih dekat. Katakanlah itu mencatat waktu mulai ketika sebuah partikel dilahirkan (dibuat). Bayangkan bidang hanya diakses ketika sebuah partikel pertama kali dibuat, dan setiap 10 detik untuk melihat apakah sebuah partikel akan mati dan terlahir kembali di lokasi acak di layar. Dalam hal ini,birth
adalah bidang yang dingin. Itu tidak diakses di loop kinerja-kritis kami.Akibatnya, data kritis kinerja aktual bukan 20 megabyte tetapi sebenarnya blok bersebelahan 12 megabyte. Memori panas aktual yang sering kita akses telah menyusut menjadi setengah ukurannya! Harapkan peningkatan yang signifikan atas solusi 24 megabyte kami yang asli (tidak perlu diukur - sudah melakukan hal semacam ini ribuan kali, tapi jangan ragu jika ragu).
Namun perhatikan apa yang kami lakukan di sini. Kami benar-benar memecahkan enkapsulasi objek partikel ini. Keadaannya sekarang dibagi antara
Particle
bidang pribadi jenis dan array paralel yang terpisah. Dan di situlah desain berorientasi objek granular menghalangi.Kami tidak dapat mengekspresikan representasi data yang optimal saat terbatas pada desain antarmuka objek tunggal, sangat granular seperti partikel tunggal, piksel tunggal, bahkan vektor 4 komponen tunggal, bahkan mungkin objek "makhluk" tunggal dalam game , dll. Kecepatan seekor cheetah akan sia-sia jika itu berdiri di sebuah pulau kecil yang hanya 2 meter persegi, dan itulah yang sering dilakukan oleh desain berorientasi objek dalam hal kinerja. Ini membatasi representasi data ke sifat yang tidak optimal.
Untuk mengambil ini lebih jauh, katakanlah karena kita hanya memindahkan partikel, kita sebenarnya dapat mengakses bidang x / y / z dalam tiga loop terpisah. Dalam hal ini, kita dapat mengambil manfaat dari intrinsik SIMD gaya SoA dengan register AVX yang dapat membuat vektor 8 operasi SPFP secara paralel. Tetapi untuk melakukan ini, kita sekarang harus menggunakan representasi ini:
Sekarang kita terbang dengan simulasi partikel, tetapi lihat apa yang terjadi pada desain partikel kita. Itu telah benar-benar dihancurkan, dan kami sekarang melihat 4 array paralel dan tidak ada objek untuk mengagregasi mereka sama sekali.
Particle
Desain berorientasi objek kami telah menjadi sayonara.Ini terjadi pada saya berkali-kali bekerja di bidang kinerja kritis di mana pengguna menuntut kecepatan dengan hanya kebenaran yang menjadi satu hal yang lebih mereka tuntut. Desain berorientasi objek kecil kecil ini harus dihancurkan, dan kerusakan berjenjang sering mengharuskan kami menggunakan strategi depresiasi lambat menuju desain yang lebih cepat.
Larutan
Skenario di atas hanya menyajikan masalah dengan desain berorientasi objek granular . Dalam kasus-kasus tersebut, kita seringkali harus menghancurkan struktur untuk mengekspresikan representasi yang lebih efisien sebagai akibat dari repetisi SoA, pemisahan medan panas / dingin, pengurangan padding untuk pola akses berurutan (padding terkadang membantu kinerja dengan akses acak) pola dalam kasus AoS, tetapi hampir selalu menjadi penghalang untuk pola akses berurutan), dll.
Namun kita dapat mengambil representasi akhir yang kita tentukan dan masih memodelkan antarmuka berorientasi objek:
Sekarang kita baik-baik saja. Kita bisa mendapatkan semua barang berorientasi objek yang kita sukai. Cheetah memiliki seluruh negara untuk berlari secepat mungkin. Desain antarmuka kami tidak lagi menjebak kami ke sudut kemacetan.
ParticleSystem
bahkan berpotensi abstrak dan menggunakan fungsi virtual. Ini diperdebatkan sekarang, kami membayar biaya overhead pada pengumpulan tingkat partikel , bukan pada tingkat per-partikel . Overhead adalah 1/1000.000 dari yang seharusnya jika kita memodelkan objek pada tingkat partikel individu.Jadi itulah solusi di bidang kritis kinerja sejati yang menangani beban berat, dan untuk semua jenis bahasa pemrograman (teknik ini menguntungkan C, C ++, Python, Java, JavaScript, Lua, Swift, dll). Dan itu tidak dapat dengan mudah dilabeli sebagai "optimasi prematur", karena ini berkaitan dengan desain antarmuka dan arsitektur . Kami tidak dapat menulis basis kode yang memodelkan partikel tunggal sebagai objek dengan muatan kapal dari dependensi klien ke a
Particle's
antarmuka publik dan kemudian berubah pikiran nanti. Saya telah melakukan banyak hal ketika dipanggil untuk mengoptimalkan basis kode lama, dan itu bisa berakhir berbulan-bulan menulis ulang puluhan ribu baris kode dengan hati-hati untuk menggunakan desain bulkier. Ini idealnya mempengaruhi bagaimana kita mendesain sesuatu di muka asalkan kita bisa mengantisipasi beban yang berat.Saya terus menggemakan jawaban ini dalam beberapa bentuk atau yang lain di banyak pertanyaan kinerja, dan terutama yang berhubungan dengan desain berorientasi objek. Desain berorientasi objek masih dapat kompatibel dengan kebutuhan kinerja permintaan tertinggi, tetapi kita harus mengubah sedikit cara berpikir kita tentangnya. Kita harus memberi cheetah ruang itu untuk berlari secepat mungkin, dan itu seringkali mustahil jika kita mendesain objek kecil mungil yang nyaris tidak menyimpan keadaan apa pun.
sumber
Ya, pola pikir berorientasi objek pasti bisa netral atau negatif ketika datang ke pemrograman kinerja tinggi, baik pada level algoritmik maupun implementasi. Jika OOP menggantikan analisis algoritmik, ini dapat mengarahkan Anda ke implementasi prematur dan, pada level terendah, abstraksi OOP harus disingkirkan.
Masalahnya berasal dari penekanan OOP pada pemikiran tentang contoh individu. Saya pikir itu adil untuk mengatakan bahwa cara berpikir OOP tentang suatu algoritma adalah dengan memikirkan serangkaian nilai tertentu dan mengimplementasikannya seperti itu. Jika itu adalah jalur level tertinggi Anda, Anda tidak mungkin mewujudkan transformasi atau restrukturisasi yang akan menghasilkan keuntungan Big O.
Pada tingkat algoritmik, sering kali dipikirkan tentang gambaran yang lebih besar dan kendala atau hubungan antara nilai-nilai yang mengarah pada perolehan Big O. Contohnya mungkin bahwa tidak ada dalam pola pikir OOP yang akan mengarahkan Anda untuk mengubah "menjumlahkan bilangan bulat yang berkelanjutan" dari satu loop ke
(max + min) * n/2
Pada tingkat implementasi, meskipun komputer "cukup cepat" untuk sebagian besar algoritma tingkat aplikasi, dalam kode tingkat kinerja rendah yang kritis, banyak yang khawatir tentang lokalitas. Sekali lagi, OOP menekankan pada berpikir tentang contoh individu dan nilai-nilai satu melewati loop dapat menjadi negatif. Dalam kode berperforma tinggi, alih-alih menulis loop langsung, Anda mungkin ingin membuka sebagian loop, mengelompokkan beberapa instruksi pemuatan di bagian atas, kemudian mentransformasikannya dalam grup, lalu menuliskannya dalam grup. Sementara itu Anda akan memperhatikan perhitungan menengah dan, sangat, untuk akses cache dan memori; masalah di mana abstraksi OOP tidak lagi valid. Dan, jika diikuti, bisa menyesatkan: pada level ini, Anda harus tahu dan memikirkan tentang representasi level mesin.
Ketika Anda melihat sesuatu seperti Intel Performance Primitives, Anda benar-benar memiliki ribuan implementasi Fast Fourier Transform, masing-masing di-tweak untuk bekerja lebih baik untuk ukuran data dan arsitektur mesin tertentu. (Menariknya, ternyata sebagian besar implementasi ini dihasilkan oleh mesin: Markus Püschel Automatic Performance Programming )
Tentu saja, seperti yang sebagian besar jawaban katakan, untuk sebagian besar pengembangan, untuk sebagian besar algoritma, OOP tidak relevan dengan kinerja. Selama Anda tidak "pesimis prematur" dan menambahkan banyak panggilan non-lokal,
this
pointer tidak ada di sini atau di sana.sumber
Ini terkait, dan sering diabaikan.
Ini bukan jawaban yang mudah, tergantung pada apa yang ingin Anda lakukan.
Beberapa algoritma lebih baik dalam kinerja menggunakan pemrograman terstruktur biasa, sementara yang lain, lebih baik menggunakan orientasi objek.
Sebelum Orientasi Objek, banyak sekolah mengajarkan (ed) desain algoritma dengan pemrograman terstruktur. Saat ini, banyak sekolah, mengajarkan pemrograman berorientasi objek, mengabaikan desain & kinerja algoritma ..
Tentu saja, ada sekolah yang mengajarkan pemrograman terstruktur, yang sama sekali tidak peduli dengan algoritma.
sumber
Kinerja semua turun ke siklus CPU dan memori pada akhirnya. Tetapi perbedaan persentase antara overhead pengiriman OOP dan enkapsulasi dan semantik pemrograman yang lebih luas mungkin atau mungkin tidak menjadi persentase yang cukup signifikan untuk membuat perbedaan yang nyata dalam kinerja aplikasi Anda. Jika suatu aplikasi disk atau data-cache-miss terikat, overhead OOP mungkin benar-benar hilang dalam kebisingan.
Namun, di loop dalam sinyal real-time dan pemrosesan gambar dan aplikasi terikat numerik lainnya, perbedaannya mungkin merupakan persentase signifikan dari siklus CPU dan memori, yang dapat membuat biaya overhead OOP jauh lebih mahal untuk dieksekusi.
Semantik dari bahasa OOP tertentu mungkin atau mungkin tidak mengekspos peluang yang cukup bagi kompiler untuk mengoptimalkan siklus-siklus itu, atau agar sirkuit prediksi cabang CPU selalu menebak dengan benar dan menutup siklus-siklus itu dengan pra-pengambilan dan pemipaan.
sumber
Desain berorientasi objek yang baik membantu saya mempercepat aplikasi. A harus menghasilkan grafik yang kompleks dengan cara algoritmik. Saya melakukannya melalui otomasi Microsoft Visio. Saya bekerja, tetapi sangat lambat. Untungnya, saya telah memasukkan level abstraksi ekstra antara logika (algoritma) dan hal-hal Visio. Komponen Visio saya mengekspos fungsinya melalui antarmuka. Ini memungkinkan saya untuk dengan mudah mengganti komponen yang lambat dengan membuat file SVG lain, yang setidaknya 50 kali lebih cepat! Tanpa pendekatan berorientasi objek yang bersih, kode untuk algoritma dan kontrol Visi akan terjerat dengan cara, yang akan mengubah perubahan menjadi mimpi buruk.
sumber