Dalam ruang lingkup pengembangan game di C ++, apa pola pilihan Anda terkait penggunaan pointer (baik itu tidak ada, mentah, tercakup, dibagikan, atau di antara pintar dan bodoh)?
Anda mungkin mempertimbangkan
- kepemilikan objek
- kemudahan penggunaan
- menyalin kebijakan
- atas
- referensi siklik
- platform target
- gunakan dengan wadah
sumber
Saya juga mengikuti jalur pemikiran "kepemilikan yang kuat". Saya ingin dengan jelas menggambarkan bahwa "kelas ini memiliki anggota ini" jika sesuai.
Saya jarang menggunakan
shared_ptr
. Jika saya melakukannya, saya menggunakan liberalweak_ptr
kapan saja saya bisa sehingga saya bisa memperlakukannya seperti pegangan untuk objek daripada meningkatkan jumlah referensi.Saya menggunakan
scoped_ptr
semua tempat. Ini menunjukkan kepemilikan yang jelas. Satu-satunya alasan saya tidak hanya membuat objek seperti itu anggota adalah karena Anda dapat meneruskan mendeklarasikannya jika mereka berada di scoped_ptr.Jika saya perlu daftar objek, saya gunakan
ptr_vector
. Ini lebih efisien dan memiliki efek samping lebih sedikit daripada menggunakanvector<shared_ptr>
. Saya pikir Anda mungkin tidak dapat meneruskan menyatakan jenis di ptr_vector (sudah lama), tetapi semantik itu membuatnya layak menurut pendapat saya. Pada dasarnya jika Anda menghapus objek dari daftar itu akan dihapus secara otomatis. Ini juga menunjukkan kepemilikan yang jelas.Jika saya perlu referensi ke sesuatu, saya mencoba menjadikannya referensi daripada pointer telanjang. Terkadang ini tidak praktis (yaitu setiap kali Anda membutuhkan referensi setelah objek dibangun). Either way, referensi menunjukkan dengan jelas bahwa Anda tidak memiliki objek, dan jika Anda mengikuti semantik pointer bersama di tempat lain maka pointer telanjang umumnya tidak menimbulkan kebingungan tambahan (terutama jika Anda mengikuti aturan "no manual delete") .
Dengan metode ini, satu permainan iPhone yang saya kerjakan hanya dapat memiliki satu
delete
panggilan, dan itu berada di jembatan Obj-C ke C ++ yang saya tulis.Secara umum saya berpendapat bahwa manajemen memori terlalu penting untuk diserahkan kepada manusia. Jika Anda dapat menghapus secara otomatis, Anda harus melakukannya. Jika overhead dari shared_ptr terlalu mahal pada saat dijalankan (dengan asumsi Anda mematikan dukungan threading, dll), Anda mungkin harus menggunakan sesuatu yang lain (yaitu pola bucket) untuk menurunkan alokasi dinamis Anda.
sumber
Gunakan alat yang tepat untuk pekerjaan itu.
Jika program Anda dapat membuang pengecualian, pastikan kode Anda adalah pengecualian. Menggunakan smart pointer, RAII, dan menghindari konstruksi 2 fase adalah titik awal yang baik.
Jika Anda memiliki referensi siklik tanpa semantik kepemilikan yang jelas, Anda dapat mempertimbangkan untuk menggunakan perpustakaan pengumpulan sampah atau refactoring desain Anda.
Perpustakaan yang baik akan memungkinkan Anda untuk kode ke konsep bukan tipe sehingga tidak masalah dalam banyak kasus jenis pointer yang Anda gunakan di luar masalah manajemen sumber daya.
Jika Anda bekerja di lingkungan multi-utas, pastikan Anda memahami jika objek Anda berpotensi dibagikan di seluruh utas. Salah satu alasan utama untuk mempertimbangkan menggunakan boost :: shared_ptr atau std :: tr1 :: shared_ptr adalah karena ia menggunakan jumlah referensi yang aman.
Jika Anda khawatir tentang alokasi jumlah referensi yang terpisah, ada banyak cara untuk mengatasi hal ini. Dengan menggunakan boost :: shared_ptr library Anda dapat mengumpulkan alokasi counter referensi atau menggunakan boost :: make_shared (preferensi saya) yang mengalokasikan objek dan jumlah referensi dalam satu alokasi sehingga mengurangi sebagian besar masalah cache miss yang dimiliki orang. Anda dapat menghindari hit kinerja memperbarui penghitungan referensi dalam kode kritis kinerja dengan memegang referensi ke objek di tingkat paling atas dan membagikan referensi langsung ke objek.
Jika Anda memerlukan kepemilikan bersama tetapi tidak ingin membayar biaya penghitungan referensi atau pengumpulan sampah, pertimbangkan untuk menggunakan objek yang tidak dapat diubah atau salinan idiom tulis.
Ingatlah bahwa jauh-jauh kemenangan kinerja terbesar Anda akan berada pada tingkat arsitektur, diikuti oleh tingkat algoritma, dan sementara masalah tingkat rendah ini sangat penting mereka harus ditangani hanya setelah Anda mengatasi masalah utama. Jika Anda berurusan dengan masalah kinerja di tingkat cache yang hilang maka Anda memiliki banyak masalah yang juga harus Anda sadari seperti berbagi palsu yang tidak ada hubungannya dengan pointer per katakan.
Jika Anda menggunakan pointer pintar hanya untuk berbagi sumber daya seperti tekstur atau model, pertimbangkan perpustakaan yang lebih khusus seperti Boost.Flyweight.
Setelah standar baru menjadi semantik langkah yang diadopsi, referensi nilai, dan penerusan yang sempurna akan membuat bekerja dengan benda dan wadah mahal jauh lebih mudah dan lebih efisien. Sampai saat itu jangan menyimpan pointer dengan semantik salinan destruktif, seperti auto_ptr atau unique_ptr, dalam sebuah Container (konsep standar). Pertimbangkan untuk menggunakan pustaka Boost.Pointer Container atau menyimpan pointer cerdas kepemilikan bersama di Kontainer. Dalam kode kritis kinerja, Anda dapat mempertimbangkan menghindari kedua hal ini demi wadah yang mengganggu seperti yang ada di Boost.Intrusive.
Platform target seharusnya tidak terlalu memengaruhi keputusan Anda. Perangkat tertanam, ponsel pintar, telepon bodoh, PC, dan konsol semuanya dapat menjalankan kode dengan baik. Persyaratan proyek seperti anggaran memori ketat atau tidak ada alokasi dinamis yang pernah / setelah dimuat adalah masalah yang lebih valid dan harus memengaruhi pilihan Anda.
sumber
Jika Anda menggunakan C ++ 0x, gunakan
std::unique_ptr<T>
.Tidak memiliki overhead kinerja, tidak seperti
std::shared_ptr<T>
yang memiliki referensi menghitung overhead. Unique_ptr memiliki penunjuknya, dan Anda dapat mentransfer kepemilikan sekitar dengan semantik langkah C ++ 0x . Anda tidak dapat menyalinnya - hanya memindahkannya.Ini juga dapat digunakan dalam wadah, misalnya
std::vector<std::unique_ptr<T>>
, yang kompatibel dengan biner dan kinerjanya identikstd::vector<T*>
, tetapi tidak akan membocorkan memori jika Anda menghapus elemen atau menghapus vektor. Ini juga memiliki kompatibilitas yang lebih baik dengan algoritma STL daripadaptr_vector
.IMO untuk banyak tujuan, ini adalah wadah yang ideal: akses acak, kecuali keamanan, mencegah kebocoran memori, overhead rendah untuk realokasi vektor (hanya bergerak di sekitar pointer di belakang layar). Sangat berguna untuk banyak keperluan.
sumber
Adalah praktik yang baik untuk mendokumentasikan kelas mana yang memiliki petunjuk apa. Lebih disukai, Anda hanya menggunakan objek normal, dan tidak ada petunjuk kapan pun Anda bisa.
Namun, ketika Anda perlu melacak sumber daya, melewati pointer adalah satu-satunya pilihan. Ada beberapa kasus:
Saya pikir itu cukup banyak mencakup bagaimana saya mengelola sumber daya saya sekarang. Biaya memori dari pointer seperti shared_ptr umumnya dua kali lipat dari biaya memori dari pointer normal. Saya tidak berpikir bahwa overhead ini terlalu besar, tetapi jika Anda memiliki sumber daya yang rendah Anda harus mempertimbangkan merancang game Anda untuk mengurangi jumlah pointer cerdas. Pada kasus lain, saya hanya mendesain prinsip-prinsip yang baik seperti peluru di atas dan profiler akan memberi tahu saya di mana saya akan membutuhkan lebih banyak kecepatan.
sumber
Ketika datang untuk meningkatkan pointer secara khusus, saya pikir mereka harus dihindari selama implementasi mereka tidak persis apa yang Anda butuhkan. Mereka datang dengan biaya yang lebih besar dari yang diperkirakan orang pada awalnya. Mereka menyediakan antarmuka yang memungkinkan Anda untuk melewati bagian vital dan penting dari memori dan manajemen sumber daya Anda.
Mengenai pengembangan perangkat lunak apa pun, saya pikir penting untuk memikirkan data Anda. Sangat penting bagaimana data Anda direpresentasikan dalam memori. Alasan untuk ini adalah bahwa kecepatan CPU telah meningkat pada tingkat yang jauh lebih besar daripada waktu akses memori. Ini sering menjadikan cache memori sebagai hambatan utama dari sebagian besar game komputer modern. Dengan menyejajarkan data Anda secara linear dalam memori sesuai dengan urutan akses, jauh lebih bersahabat dengan cache. Solusi semacam ini sering mengarah pada desain yang lebih bersih, kode lebih sederhana dan kode pasti yang lebih mudah untuk di-debug. Pointer pintar dengan mudah menyebabkan alokasi sumber daya memori dinamis sering, ini menyebabkan mereka tersebar di seluruh memori.
Ini bukan optimasi prematur, ini adalah keputusan sehat yang dapat dan harus diambil sedini mungkin. Ini adalah pertanyaan tentang pemahaman arsitektur perangkat keras yang akan dijalankan oleh perangkat lunak Anda dan ini penting.
Sunting: Ada beberapa hal yang perlu dipertimbangkan mengenai kinerja dari shared-pointer:
sumber
Saya cenderung menggunakan smart pointer di mana-mana. Saya tidak yakin apakah ini ide yang benar-benar bagus, tetapi saya malas, dan saya tidak dapat melihat kelemahan nyata [kecuali jika saya ingin melakukan aritmatika pointer C-style]. Saya menggunakan boost :: shared_ptr karena saya tahu saya bisa menyalinnya - jika dua entitas berbagi gambar, maka jika satu mati yang lain tidak akan kehilangan gambar juga.
Kelemahan dari ini adalah jika satu objek menghapus sesuatu yang ia tunjuk dan miliki, tetapi sesuatu yang lain juga menunjuk ke sana, maka itu tidak dihapus.
sumber
Manfaat manajemen memori dan dokumentasi yang disediakan oleh smart pointer yang baik berarti saya menggunakannya secara teratur. Namun ketika profiler memasang pipa dan memberi tahu saya penggunaan khusus yang menghabiskan biaya, saya akan kembali ke manajemen pointer yang lebih neolitik.
sumber
Saya tua, oldskool, dan penghitung siklus. Dalam pekerjaan saya sendiri, saya menggunakan pointer mentah dan tidak ada alokasi dinamis saat runtime (kecuali kolam itu sendiri). Semuanya dikumpulkan, dan kepemilikannya sangat ketat dan tidak pernah dapat dipindahtangankan, jika benar-benar diperlukan saya menulis pengalokasi blok kecil khusus. Saya memastikan bahwa ada keadaan selama pertandingan untuk setiap kelompok untuk membersihkan dirinya sendiri. Ketika sesuatu menjadi berbulu, saya membungkus benda-benda di pegangan sehingga saya bisa memindahkannya, tetapi saya lebih suka tidak. Wadah adalah adat dan tulang yang sangat telanjang. Saya juga tidak menggunakan kembali kode.
Walaupun saya tidak akan pernah memperdebatkan keutamaan dari semua pointer cerdas dan wadah dan iterator dan yang lainnya, saya dikenal karena mampu membuat kode dengan sangat cepat (dan cukup dapat diandalkan - meskipun tidak disarankan bagi orang lain untuk masuk ke kode saya karena alasan yang agak jelas, seperti serangan jantung dan mimpi buruk abadi).
Di tempat kerja, tentu saja, semuanya berbeda, kecuali saya membuat prototipe, yang untungnya saya bisa melakukan banyak hal.
sumber
Hampir tidak ada meskipun ini diakui sebagai jawaban yang aneh, dan mungkin tidak cocok untuk semua orang.
Tetapi saya merasa jauh lebih berguna dalam kasus pribadi saya untuk menyimpan semua instance dari tipe tertentu dalam urutan pusat, akses acak (thread-safe), dan sebagai gantinya bekerja dengan indeks 32-bit (alamat relatif, yaitu) , daripada petunjuk absolut.
Sebagai permulaan:
T
,, tidak akan pernah terlalu tersebar di memori. Itu cenderung mengurangi kesalahan cache untuk semua jenis pola akses, bahkan melintasi struktur yang ditautkan seperti pohon jika node dihubungkan bersama menggunakan indeks daripada pointer.Yang mengatakan, kenyamanan adalah kelemahan serta keamanan jenis. Kita tidak bisa mengakses sebuah instance dari
T
tanpa akses ke kedua kontainer dan indeks. Dan data lama yang biasa tidakint32_t
memberi tahu kita tentang tipe data apa yang dirujuknya, jadi tidak ada tipe yang aman. Kami tidak sengaja dapat mencoba mengaksesBar
menggunakan indeks untukFoo
. Untuk mengurangi masalah kedua saya sering melakukan hal semacam ini:Kelihatannya konyol tapi itu memberi saya kembali jenis keamanan sehingga orang tidak dapat secara tidak sengaja mencoba mengakses
Bar
melalui indeksFoo
tanpa kesalahan kompiler. Untuk sisi kenyamanan, saya hanya menerima sedikit ketidaknyamanan.Hal lain yang dapat menjadi ketidaknyamanan utama bagi orang-orang adalah bahwa saya tidak dapat menggunakan polimorfisme berbasis warisan gaya OOP, karena itu akan membutuhkan penunjuk dasar yang dapat menunjuk ke semua jenis subtipe berbeda dengan ukuran dan persyaratan penyelarasan yang berbeda. Tapi saya tidak menggunakan banyak warisan hari ini - lebih suka pendekatan ECS.
Adapun
shared_ptr
, saya mencoba untuk tidak menggunakannya terlalu banyak. Sebagian besar waktu saya merasa tidak masuk akal untuk berbagi kepemilikan, dan melakukan hal itu secara sembarangan dapat menyebabkan kebocoran logis. Seringkali setidaknya pada level tinggi, satu hal cenderung menjadi milik satu hal. Di mana saya sering merasa tergoda untuk menggunakannyashared_ptr
adalah memperpanjang umur suatu objek di tempat-tempat yang tidak terlalu banyak berurusan dengan kepemilikan, seperti hanya fungsi lokal di utas untuk memastikan objek tidak hancur sebelum utas selesai menggunakannya.Untuk mengatasi masalah itu, alih-alih menggunakan
shared_ptr
atau GC atau sesuatu seperti itu, saya sering memilih tugas jangka pendek yang berjalan dari kumpulan utas, dan membuatnya jadi jika utas itu meminta untuk menghancurkan suatu objek, bahwa penghancuran yang sebenarnya ditangguhkan ke brankas. waktu ketika sistem dapat memastikan bahwa tidak ada utas yang perlu mengakses jenis objek tersebut.Saya kadang-kadang masih menggunakan penghitungan ulang tetapi memperlakukannya seperti strategi upaya terakhir. Dan ada beberapa kasus di mana benar-benar masuk akal untuk berbagi kepemilikan, seperti penerapan struktur data yang persisten, dan di sana saya merasa masuk akal untuk
shared_ptr
segera meraihnya.Jadi bagaimanapun, saya kebanyakan menggunakan indeks, dan menggunakan pointer mentah dan pintar hemat. Saya suka indeks dan jenis pintu yang terbuka ketika Anda tahu benda Anda disimpan secara bersebelahan, dan tidak tersebar di ruang memori.
sumber