Apakah rekomendasi Microsoft untuk menggunakan properti C # dapat diterapkan untuk pengembangan game?

26

Saya mendapatkan bahwa kadang-kadang Anda membutuhkan properti, seperti:

public int[] Transitions { get; set; }

atau:

[SerializeField] private int[] m_Transitions;
public int[] Transitions { get { return m_Transitions; } set { m_Transitions = value; } }

Tapi kesan saya adalah bahwa dalam pengembangan game, kecuali jika Anda memiliki alasan untuk melakukan sebaliknya, semuanya harus hampir secepat mungkin. Kesan saya dari hal-hal yang saya baca adalah Unity 3D tidak yakin untuk inline akses melalui properti, jadi menggunakan properti akan menghasilkan panggilan fungsi tambahan.

Apakah saya benar untuk selalu mengabaikan saran Visual Studio untuk tidak menggunakan bidang publik? (Perhatikan bahwa kami menggunakan int Foo { get; private set; }jika ada keuntungan dalam menunjukkan penggunaan yang dimaksud.)

Saya sadar bahwa optimasi prematur itu buruk, tetapi seperti yang saya katakan, dalam pengembangan game Anda tidak membuat sesuatu yang lambat ketika upaya serupa bisa membuatnya cepat.

piojo
sumber
34
Selalu verifikasi dengan profiler sebelum percaya ada sesuatu yang "lambat". Saya diberitahu ada sesuatu yang "lambat" dan profiler mengatakan kepada saya bahwa itu harus mengeksekusi 500.000.000 kali kehilangan setengah detik. Karena tidak akan pernah mengeksekusi lebih dari beberapa ratus kali dalam satu frame, saya memutuskan itu tidak relevan.
Almo
5
Saya telah menemukan bahwa sedikit abstraksi di sana-sini membantu menerapkan pengoptimalan tingkat rendah secara lebih luas. Sebagai contoh, saya telah melihat banyak proyek C sederhana menggunakan berbagai jenis tautan-daftar, yang sangat lambat dibandingkan dengan array yang berdekatan (misalnya dukungan untuk ArrayList). Ini karena C tidak memiliki generik, dan para programmer C ini mungkin merasa lebih mudah untuk berulang kali mengimplementasikan kembali daftar yang ditautkan daripada melakukan hal yang sama untuk ArrayListsalgoritma pengubahan ukuran. Jika Anda mendapatkan optimasi tingkat rendah yang bagus, enkapsulasi!
hegel5000
3
@Almo Apakah Anda menerapkan logika yang sama untuk operasi yang terjadi di 10.000 tempat berbeda? Karena itulah akses anggota kelas. Secara logis Anda harus mengalikan biaya kinerja dengan 10 ribu sebelum memutuskan bahwa kelas optimasi tidak masalah. Dan 0,5 ms adalah aturan praktis yang lebih baik untuk ketika kinerja game Anda akan hancur, tidak setengah detik. Maksud Anda masih ada, meskipun saya tidak berpikir tindakan yang tepat sejelas yang Anda lakukan.
piojo
5
Anda memiliki 2 kuda. Ras mereka.
Tiang
@ piojo saya tidak. Beberapa pemikiran harus selalu membahas hal-hal ini. Dalam kasus saya, itu untuk loop dalam kode UI. Satu contoh UI, beberapa loop, beberapa iterasi. Sebagai catatan, saya meningkatkan komentar Anda. :)
Almo

Jawaban:

44

Tapi kesan saya adalah bahwa dalam pengembangan game, kecuali jika Anda memiliki alasan untuk melakukan sebaliknya, semuanya harus hampir secepat mungkin.

Belum tentu. Sama seperti dalam perangkat lunak aplikasi, ada kode dalam game yang kinerjanya kritis dan kode yang tidak.

Jika kode dijalankan beberapa ribu kali per frame, maka optimasi tingkat rendah seperti itu mungkin masuk akal. (Meskipun Anda mungkin ingin memeriksa dulu apakah benar-benar perlu untuk menyebutnya sesering itu. Kode tercepat adalah kode yang tidak Anda jalankan)

Jika kode dieksekusi sekali setiap beberapa detik, maka keterbacaan dan pemeliharaan jauh lebih penting daripada kinerja.

Yang saya baca adalah Unity 3D tidak yakin untuk inline akses melalui properti, jadi menggunakan properti akan menghasilkan panggilan fungsi tambahan.

Dengan properti sepele dalam gaya int Foo { get; private set; }Anda dapat cukup yakin bahwa kompiler akan mengoptimalkan pembungkus properti. Tapi:

  • jika Anda benar-benar memiliki implementasi, maka Anda tidak bisa yakin lagi. Semakin kompleks implementasi itu, semakin kecil kemungkinan kompiler akan dapat menyejajarkannya.
  • Properti dapat menyembunyikan kerumitan. Ini adalah pedang bermata dua. Di satu sisi, itu membuat kode Anda lebih mudah dibaca dan diubah. Tetapi di sisi lain, pengembang lain yang mengakses properti kelas Anda mungkin berpikir mereka hanya mengakses satu variabel dan tidak menyadari berapa banyak kode yang sebenarnya Anda sembunyikan di balik properti itu. Ketika sebuah properti mulai menjadi mahal secara komputasi, maka saya cenderung untuk mengubahnya menjadi metode GetFoo()/ eksplisit SetFoo(value): Untuk menyiratkan bahwa ada lebih banyak terjadi di belakang layar. (atau jika saya perlu lebih yakin bahwa orang lain mendapatkan petunjuk: CalculateFoo()/ SwitchFoo(value))
Philipp
sumber
Mengakses bidang dalam bidang publik non-baca-saja dari tipe struktur cepat, bahkan jika bidang itu dalam struktur yang ada dalam struktur dll., Asalkan objek tingkat atas adalah kelas non-baca-saja bidang atau variabel bebas berdiri yang tidak hanya baca. Struktur dapat menjadi cukup besar tanpa mempengaruhi kinerja jika kondisi ini berlaku. Mematahkan pola ini akan menyebabkan perlambatan proporsional dengan ukuran struktur.
supercat
5
"maka saya cenderung untuk mengubahnya menjadi metode GetFoo () / SetFoo (nilai) yang eksplisit:" - Poin bagus, saya cenderung memindahkan operasi besar dari properti ke metode.
Mark Rogers
Apakah ada cara untuk mengonfirmasi bahwa kompiler Unity 3D membuat optimasi yang Anda sebutkan (di IL2CPP dan Mono build untuk Android)?
piojo
9
@ Piojo Seperti kebanyakan pertanyaan kinerja, jawaban paling sederhana adalah "lakukan saja". Anda memiliki kode yang siap - uji perbedaan antara memiliki bidang dan memiliki properti. Detail implementasi hanya layak diselidiki ketika Anda benar-benar dapat mengukur perbedaan penting antara kedua pendekatan tersebut. Dalam pengalaman saya, jika Anda menulis semuanya secepat mungkin, Anda membatasi optimasi tingkat tinggi yang tersedia, dan hanya mencoba untuk balapan bagian tingkat rendah ("tidak dapat melihat hutan untuk pohon"). Misalnya menemukan cara untuk melompati Updateseluruhnya akan menghemat lebih banyak waktu daripada membuat Updatecepat.
Luaan
9

Jika ragu, gunakan praktik terbaik.

Anda ragu.


Itu jawaban yang mudah. Realitas lebih kompleks, tentu saja.

Pertama, ada mitos bahwa pemrograman game adalah pemrograman kinerja ultra super tinggi dan semuanya harus secepat mungkin. Saya mengklasifikasikan pemrograman game sebagai pemrograman yang sadar kinerja . Pengembang harus menyadari kendala kinerja dan bekerja di dalamnya.

Kedua, ada mitos yang membuat setiap hal secepat mungkin adalah apa yang membuat program berjalan cepat. Pada kenyataannya, mengidentifikasi dan mengoptimalkan hambatan adalah apa yang membuat suatu program cepat, dan itu jauh lebih mudah / lebih murah / lebih cepat dilakukan jika kode mudah dipertahankan dan dimodifikasi; kode yang sangat optimal sulit untuk dipertahankan dan dimodifikasi. Oleh karena itu untuk menulis program yang sangat optimal, Anda harus menggunakan optimasi kode ekstrim sesering yang diperlukan dan sesering mungkin.

Ketiga, manfaat dari properti dan aksesor adalah mereka kuat untuk diubah, dan dengan demikian membuat kode lebih mudah untuk dipelihara dan dimodifikasi - yang merupakan sesuatu yang Anda butuhkan untuk menulis program cepat. Ingin melempar pengecualian setiap kali seseorang ingin menetapkan nilai Anda ke nol? Gunakan setter. Ingin mengirim pemberitahuan kapan pun nilainya berubah? Gunakan setter. Ingin mencatat nilai baru? Gunakan setter. Ingin melakukan inisialisasi malas? Gunakan pengambil.

Dan keempat, secara pribadi saya tidak mengikuti praktik terbaik Microsoft dalam hal ini. Jika saya membutuhkan properti dengan getter dan setter publik dan sepele , saya hanya menggunakan bidang, dan cukup mengubahnya ke properti saat saya membutuhkannya. Namun, saya sangat jarang membutuhkan properti sepele seperti itu - jauh lebih umum bahwa saya ingin memeriksa kondisi batas, atau ingin membuat setter pribadi.

Peter - Unban Robert Harvey
sumber
2

Sangat mudah untuk .net runtime ke inline properti sederhana, dan seringkali memang demikian. Namun, inlining ini biasanya dinonaktifkan oleh profiler, oleh karena itu mudah disesatkan dan berpikir bahwa mengubah properti publik menjadi bidang publik akan mempercepat perangkat lunak.

Dalam kode yang dipanggil oleh kode lain yang tidak akan dikompilasi ulang ketika kode Anda dikompilasi ulang ada kasus yang baik untuk menggunakan properti, karena mengubah dari bidang ke properti adalah perubahan yang melanggar. Tetapi untuk sebagian besar kode, selain bisa mengatur breakpoint pada get / set, saya belum pernah melihat manfaat dalam menggunakan properti sederhana dibandingkan dengan bidang publik.

Alasan utama saya menggunakan properti dalam 10 tahun saya sebagai programmer C # profesional adalah untuk menghentikan programmer lain mengatakan kepada saya bahwa saya tidak menjaga standar pengkodean. Ini adalah tradeoff yang bagus, karena jarang ada alasan kuat untuk tidak menggunakan properti.

Ian Ringrose
sumber
2

Structs dan bidang telanjang akan memudahkan interoperabilitas dengan beberapa API yang tidak dikelola. Seringkali Anda akan menemukan bahwa API tingkat rendah ingin mengakses nilai dengan referensi, yang bagus untuk kinerja (karena kami menghindari salinan yang tidak perlu). Menggunakan properti adalah hambatan untuk itu, dan sering kali perpustakaan pembungkus akan melakukan salinan untuk kemudahan penggunaan, dan kadang-kadang untuk keamanan.

Karena itu, Anda akan sering mendapatkan kinerja yang lebih baik dengan jenis vektor dan matriks yang tidak memiliki properti tetapi bidang telanjang.


Praktik terbaik tidak dibuat dalam vaksin. Meskipun ada beberapa kultus kargo, secara umum praktik terbaik ada untuk alasan yang baik.

Dalam hal ini, kami memiliki pasangan:

  • Properti memungkinkan Anda untuk mengubah implementasi tanpa mengubah kode klien (pada tingkat biner, dimungkinkan untuk mengubah bidang ke properti tanpa mengubah kode klien pada tingkat sumber, namun itu akan dikompilasi ke sesuatu yang berbeda setelah perubahan) . Itu berarti, bahwa dengan menggunakan properti dari awal, kode yang mereferensikan milik Anda tidak perlu dikompilasi ulang hanya untuk mengubah apa yang properti lakukan secara internal.

  • Jika tidak semua nilai yang mungkin dari bidang tipe Anda adalah status yang valid, maka Anda tidak ingin memaparkannya ke kode klien yang diubah oleh kode itu. Dengan demikian, jika beberapa kombinasi nilai tidak valid, Anda ingin merahasiakan bidang tersebut (atau internal).

Saya telah mengatakan kode klien. Itu berarti kode yang memanggil Anda. Jika Anda tidak membuat perpustakaan (atau bahkan membuat perpustakaan tetapi menggunakan internal alih-alih publik), Anda biasanya dapat menghindarinya dan disiplin yang baik. Dalam situasi itu, praktik terbaik menggunakan properti ada untuk mencegah Anda menembak diri sendiri. Selain itu, jauh lebih mudah untuk berpikir tentang kode jika Anda dapat melihat semua tempat di mana bidang dapat berubah dalam satu file, daripada harus khawatir pada apa pun atau tidak itu sedang dimodifikasi di tempat lain. Bahkan, properti juga bagus untuk meletakkan breakpoint ketika Anda mencari tahu apa yang salah.


Ya, ada nilai melihat apa yang sedang dilakukan di industri. Namun, apakah Anda memiliki motivasi untuk menentang praktik terbaik? atau apakah Anda hanya menentang praktik terbaik - membuat kode lebih sulit untuk dipikirkan - hanya karena orang lain melakukannya? Ah, omong-omong, "orang lain melakukannya" adalah bagaimana Anda memulai kultus kargo.


Jadi ... Apakah gim Anda berjalan lambat? Anda lebih baik mencurahkan waktu untuk mencari tahu kemacetan dan memperbaikinya, daripada berspekulasi apa yang bisa terjadi. Anda dapat yakin bahwa kompiler akan melakukan banyak optimasi, karena itu, kemungkinan Anda melihat masalah yang salah.

Di sisi lain, jika Anda memutuskan apa yang harus dilakukan untuk memulai, Anda harus khawatir tentang apa algoritma dan struktur data terlebih dahulu, daripada khawatir tentang detail yang lebih kecil seperti bidang vs properti.


Akhirnya, apakah Anda mendapatkan sesuatu dengan menentang praktik terbaik?

Khusus untuk kasus Anda (Unity dan Mono untuk Android), apakah Unity mengambil nilai dengan referensi? Jika tidak, itu akan menyalin nilai pula, tidak ada peningkatan kinerja di sana.

Jika ya, jika Anda meneruskan data ini ke API yang membutuhkan referensi. Apakah masuk akal untuk membuat bidang menjadi publik, atau Anda dapat membuat jenisnya dapat memanggil API secara langsung?

Ya, tentu saja, mungkin ada optimasi yang bisa Anda lakukan dengan menggunakan struct dengan bidang telanjang. Misalnya, Anda mengaksesnya dengan pointer Span<T> , atau serupa. Mereka juga kompak pada memori, membuatnya mudah untuk diserialisasi untuk mengirim melalui jaringan atau dimasukkan ke dalam penyimpanan permanen (dan ya itu adalah salinan).

Sekarang, sudahkah Anda memilih algoritme dan struktur yang tepat, jika ternyata menjadi hambatan, maka Anda memutuskan apa cara terbaik untuk memperbaikinya ... yang bisa berupa struct dengan bidang telanjang atau tidak. Anda akan dapat khawatir tentang itu jika dan kapan itu terjadi. Sementara itu Anda bisa khawatir tentang hal-hal yang lebih penting seperti membuat game yang baik atau menyenangkan layak dimainkan.

Theraot
sumber
1

... Kesan saya adalah bahwa dalam pengembangan game, kecuali jika Anda memiliki alasan untuk melakukan sebaliknya, semuanya harus hampir secepat mungkin.

:

Saya sadar bahwa optimasi prematur itu buruk, tetapi seperti yang saya katakan, dalam pengembangan game Anda tidak membuat sesuatu yang lambat ketika upaya serupa bisa membuatnya cepat.

Anda benar untuk mengetahui bahwa optimasi prematur itu buruk tetapi itu hanya setengah dari cerita (lihat kutipan lengkap di bawah). Bahkan dalam pengembangan game, tidak semua harus secepat mungkin.

Pertama membuatnya bekerja. Hanya dengan begitu, optimalkan bit yang membutuhkannya. Itu bukan untuk mengatakan bahwa draf pertama dapat menjadi berantakan atau tidak efisien tidak perlu tetapi optimasi itu tidak menjadi prioritas pada tahap ini.

" Masalah sebenarnya adalah bahwa programmer telah menghabiskan terlalu banyak waktu untuk mengkhawatirkan efisiensi di tempat yang salah dan pada waktu yang salah ; optimalisasi prematur adalah akar dari semua kejahatan (atau setidaknya sebagian besar) dalam pemrograman." Donald Knuth, 1974.

Vincent O'Sullivan
sumber
1

Untuk hal-hal dasar seperti itu, pengoptimal C # akan tetap melakukannya dengan benar. Jika Anda mengkhawatirkannya (dan jika Anda mengkhawatirkannya lebih dari 1% dari kode Anda , Anda melakukan kesalahan ) sebuah atribut dibuat untuk Anda. Atribut [MethodImpl(MethodImplOptions.AggressiveInlining)]sangat meningkatkan kemungkinan metode akan diuraikan (pengambil properti dan setter adalah metode).

Joshua
sumber
0

Mengekspos anggota tipe struktur sebagai properti dan bukan bidang akan sering menghasilkan kinerja dan semantik yang buruk, terutama dalam kasus di mana struktur sebenarnya diperlakukan sebagai struktur, bukan sebagai "objek unik".

Struktur dalam .NET adalah kumpulan bidang yang direkam bersama, dan yang untuk beberapa tujuan dapat diperlakukan sebagai satu unit. NET Framework. Dirancang untuk memungkinkan struktur untuk digunakan dalam cara yang sama seperti objek kelas, dan ada saat-saat ini bisa nyaman.

Sebagian besar saran seputar struktur didasarkan pada gagasan bahwa pemrogram akan ingin menggunakan struktur sebagai objek, daripada sebagai kumpulan bidang yang direkam bersama-sama. Di sisi lain, ada banyak kasus di mana berguna untuk memiliki sesuatu yang berperilaku sebagai sekelompok bidang yang direkam. Jika itu yang dibutuhkan, saran .NET akan menambah pekerjaan yang tidak perlu untuk programmer dan kompiler, menghasilkan kinerja dan semantik yang lebih buruk daripada hanya menggunakan struktur secara langsung.

Prinsip terbesar yang harus diingat ketika menggunakan struktur adalah:

  1. Jangan melewati atau mengembalikan struktur berdasarkan nilai atau menyebabkannya disalin secara tidak perlu.

  2. Jika prinsip # 1 dipatuhi, struktur besar akan berkinerja sama baiknya dengan yang kecil.

Jika Alphablobadalah struktur yang berisi 26 publik intbidang bernama az, dan pengambil properti abyang mengembalikan jumlah yang adan bbidang, kemudian diberi Alphablob[] arr; List<Alphablob> list;, kode int foo = arr[0].ab + list[0].ab;akan perlu membaca bidang adan bdari arr[0], tetapi akan perlu untuk membaca semua 26 bidang list[0]meskipun akan abaikan semuanya kecuali dua. Jika seseorang ingin memiliki koleksi seperti daftar generik yang dapat bekerja secara efisien dengan struktur seperti alphaBlob, seseorang harus mengganti pengambil yang diindeks dengan metode:

delegate ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
actOnItem<TParam>(int index, ActByRef<T, TParam> proc, ref TParam param);

yang kemudian akan memohon proc(ref backingArray[index], ref param);. Mengingat koleksi tersebut, jika salah satu Menggantikan sum = myCollection[0].ab;dengan

int result;
myCollection.actOnItem<int>(0, 
  ref (ref alphaBlob item, ref int dest)=>dest = item,
  ref result);

yang akan menghindari kebutuhan untuk menyalin bagian dari alphaBlobyang tidak akan digunakan dalam pengambil properti. Karena delegasi yang lulus tidak mengakses apa pun selain refparameternya, kompiler dapat melewati delegasi statis.

Sayangnya, .NET Framework tidak mendefinisikan salah satu dari jenis delegasi yang diperlukan untuk membuat hal ini berfungsi dengan baik, dan sintaks akhirnya menjadi berantakan. Di sisi lain, pendekatan ini memungkinkan untuk menghindari penyalinan struktur yang tidak perlu, yang pada gilirannya akan membuatnya praktis untuk melakukan tindakan di tempat pada struktur besar yang disimpan dalam array, yang merupakan cara paling efisien untuk mengakses penyimpanan.

supercat
sumber
Hai, apakah Anda memposting jawaban Anda ke halaman yang salah?
piojo
@pojo: Mungkin saya seharusnya memperjelas bahwa tujuan jawabannya adalah untuk mengidentifikasi situasi di mana menggunakan properti bukan bidang adalah buruk, dan mengidentifikasi bagaimana seseorang dapat mencapai kinerja dan keuntungan semantik bidang, sambil tetap merangkum akses.
supercat
@piojo: Apakah hasil edit saya memperjelas?
supercat
"Akan perlu membaca semua 26 bidang daftar [0] meskipun itu akan mengabaikan semua kecuali dua dari mereka." apa? Apakah kamu yakin Sudahkah Anda memeriksa MSIL? C # hampir selalu melewati tipe kelas dengan referensi.
pjc50
@ pjc50: Membaca properti menghasilkan hasil berdasarkan nilai. Jika pengambil getar terindeks untuk listdan pengambil properti untuk abkeduanya di-in-line, dimungkinkan untuk JITter untuk mengenali bahwa hanya anggota adan bdiperlukan, tetapi saya tidak mengetahui versi JITter yang benar-benar melakukan hal-hal seperti itu.
supercat