Tetapi mungkinkah OOP ini menjadi kerugian bagi perangkat lunak berdasarkan kinerja, yaitu seberapa cepat program dijalankan?
Sering ya !!! TAPI...
Dengan kata lain, dapatkah banyak referensi antara banyak objek yang berbeda, atau menggunakan banyak metode dari banyak kelas, menghasilkan implementasi yang "berat"?
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 daripada int
(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 ditandai final
.
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:
class Particle
{
public:
...
private:
double birth; // 8 bytes
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
/*padding*/ // 4 bytes of padding
};
Particle particles[1000000]; // 1mil particles (~24 megs)
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:
class Particle
{
public:
...
private:
float x; // 4 bytes
float y; // 4 bytes
float z; // 4 bytes
};
Particle particles[1000000]; // 1mil particles (~12 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
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:
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
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:
// Represents a collection of particles.
class ParticleSystem
{
public:
...
private:
double particle_birth[1000000]; // 1mil particle births (~8 bytes)
float particle_x[1000000]; // 1mil particle X positions (~4 megs)
float particle_y[1000000]; // 1mil particle Y positions (~4 megs)
float particle_z[1000000]; // 1mil particle Z positions (~4 megs)
};
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 aParticle'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.