Bagaimana saya harus menjelaskan GC ketika membangun game dengan Unity?

15

* Sejauh yang saya tahu, Unity3D untuk iOS didasarkan pada runtime Mono dan Mono hanya memiliki tanda generasi & sapuan GC.

Sistem GC ini tidak dapat menghindari waktu GC yang menghentikan sistem game. Penggabungan instance dapat mengurangi ini tetapi tidak sepenuhnya, karena kami tidak dapat mengontrol instantiasi yang terjadi di pustaka kelas dasar CLR. Contoh kecil dan sering disembunyikan akan meningkatkan waktu GC akhirnya tidak menentukan. Memaksa GC yang lengkap secara berkala akan sangat menurunkan kinerja (dapatkah Mono memaksa GC yang lengkap, sebenarnya?)

Jadi, bagaimana saya bisa menghindari waktu GC ini ketika menggunakan Unity3D tanpa menurunkan kinerja besar?

Eonil
sumber
3
Unity Pro Profiler terbaru termasuk berapa banyak sampah yang dihasilkan dalam panggilan fungsi tertentu. Anda dapat menggunakan alat ini untuk membantu melihat tempat lain yang mungkin menghasilkan sampah yang mungkin tidak langsung terlihat sebaliknya.
Tetrad

Jawaban:

18

Untungnya, seperti yang Anda tunjukkan, build COMPACT Mono menggunakan GC generasi (sangat kontras dengan yang Microsoft, seperti WinMo / WinPhone / XBox, yang hanya mempertahankan daftar datar).

Jika gim Anda sederhana, GC harus menanganinya dengan baik, tetapi berikut adalah beberapa petunjuk yang mungkin ingin Anda perhatikan.

Optimalisasi Dini

Pertama, pastikan ini benar-benar masalah bagi Anda sebelum mencoba memperbaikinya.

Jenis referensi mahal Pooling

Anda harus mengumpulkan jenis referensi yang sering Anda buat, atau yang memiliki struktur dalam. Contoh masing-masing adalah:

  • Dibuat sering: BulletObjek dalam permainan peluru-neraka .
  • Struktur dalam: Pohon keputusan untuk implementasi AI.

Anda harus menggunakan Stacksebagai kumpulan Anda (tidak seperti kebanyakan implementasi yang menggunakan a Queue). Alasan untuk ini adalah karena dengan aStack jika Anda mengembalikan objek ke kolam dan sesuatu yang lain segera meraihnya; itu akan memiliki peluang yang jauh lebih tinggi untuk berada di halaman aktif - atau bahkan di cache CPU jika Anda beruntung. Hanya saja itu sedikit lebih cepat. Selain itu selalu ukuran batas kolam Anda (abaikan saja 'checkin' jika batas Anda telah terlampaui).

Hindari Membuat Daftar Baru untuk Membersihkan Mereka

Jangan membuat yang baru Listketika Anda benar-benar bermaksud untuk Clear()itu. Anda dapat menggunakan kembali array backend dan menyimpan banyak alokasi dan salinan array. Demikian pula dengan percobaan ini dan buat daftar dengan kapasitas awal yang bermakna (ingat, ini bukan batas - hanya kapasitas awal) - tidak perlu akurat, hanya perkiraan. Ini pada dasarnya berlaku untuk semua jenis koleksi - kecuali untuk aLinkedList .

Gunakan Struct Arrays (atau Daftar) Di Mana Mungkin

Anda mendapatkan sedikit manfaat dari penggunaan struct (atau tipe nilai secara umum) jika Anda membagikannya di antara objek. Sebagai contoh, dalam kebanyakan sistem partikel 'baik', partikel individu disimpan dalam array besar: array dan dan indeks dilewatkan di sekitar bukan partikel itu sendiri. Alasan ini bekerja sangat baik adalah karena ketika GC perlu mengumpulkan array, ia dapat melewatkan konten sepenuhnya (itu array primitif - tidak ada hubungannya di sini). Jadi alih-alih melihat 10.000 objek, GC hanya perlu melihat 1 array: untung besar! Sekali lagi, ini hanya akan bekerja dengan tipe nilai .

Setelah RoyT. memberikan beberapa umpan balik yang layak dan konstruktif yang saya rasa perlu saya kembangkan lebih lanjut. Anda hanya harus menggunakan teknik ini ketika Anda berurusan dengan entitas dalam jumlah besar (ribuan hingga puluhan ribu). Selain itu, harus berupa struct yang tidak boleh memiliki bidang jenis referensi dan harus hidup dalam array yang diketik secara eksplisit. Bertentangan dengan umpan baliknya, kami menempatkannya dalam array yang sangat mungkin merupakan bidang dalam kelas - yang berarti bahwa itu akan mendapatkan heap (kami tidak berusaha untuk menghindari alokasi tumpukan - hanya menghindari pekerjaan GC). Kami benar-benar peduli adalah kenyataan bahwa itu adalah sepotong memori yang berdekatan dengan banyak nilai yang dapat dilihat dengan mudah oleh GC dalam suatu O(1)operasi alih-alih O(n)operasi.

Anda juga harus mengalokasikan array ini sedekat mungkin dengan startup aplikasi Anda untuk mengurangi kemungkinan terjadinya fragmentasi , atau kerja berlebihan ketika GC mencoba untuk memindahkan potongan-potongan ini, (dan pertimbangkan untuk menggunakan daftar tautan hybrid alih-alih Listjenis bawaan. ).

GC.Collect ()

Ini jelas merupakan cara TERBAIK untuk menembak diri sendiri (lihat: "Pertimbangan Kinerja") dengan GC generasi. Anda hanya boleh menyebutnya ketika Anda telah membuat jumlah EXTREME sampah - dan satu contoh di mana itu bisa menjadi masalah adalah hanya setelah Anda memuat konten untuk level - dan bahkan kemudian Anda mungkin hanya perlu mengumpulkan generasi pertama ( GC.Collect(0);) mudah-mudahan mencegah mempromosikan objek ke generasi ketiga.

IDisposable dan Field Nulling

Berguna untuk membatalkan bidang ketika Anda tidak lagi membutuhkan objek (lebih-lebih pada objek terbatas). Alasannya adalah dalam rincian tentang cara kerja GC: hanya menghapus objek yang tidak di-rooting (yaitu direferensikan) bahkan jika objek itu akan dicabut karena objek lain yang dihapus dalam koleksi saat ini ( catatan: ini tergantung pada GC rasa digunakan - beberapa benar-benar membersihkan rantai). Selain itu, jika suatu objek bertahan koleksi, itu segera dipromosikan ke generasi berikutnya - ini berarti bahwa setiap benda yang tertinggal di ladang akan dipromosikan selama koleksi. Setiap generasi berturut-turut secara eksponensial lebih mahal untuk dikumpulkan (dan jarang terjadi).

Ambil contoh berikut:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// G1 Collection
MyNestObject (G2) -> MyFurtherNestedObject (G2)
// G2 Collection
MyFurtherNestedObject (G3)

Jika MyFurtherNestedObjectterdapat objek multi-megabyte, Anda dapat dijamin bahwa GC tidak akan melihatnya cukup lama - karena Anda secara tidak sengaja mempromosikannya ke G3. Bandingkan dengan contoh ini:

MyObject (G1) -> MyNestedObject (G1) -> MyFurtherNestedObject (G1)
// Dispose
MyObject (G1)
MyNestedObject (G1)
MyFurtherNestedObject (G1)
// G1 Collection

Pola Disposer membantu Anda mengatur cara yang dapat diprediksi untuk meminta objek untuk menghapus bidang pribadi mereka. Sebagai contoh:

public class MyClass : IDisposable
{
    private MyNestedType _nested;

    // A finalizer is only needed IF YOU CONTROL UNMANAGED RESOURCES
    // ~MyClass() { }

    public void Dispose()
    {
       _nested = null;
    }
}
Jonathan Dickinson
sumber
1
WTF tentang apa? .NET Framework pada Windows benar-benar bersifat generasional. Kelas mereka bahkan memiliki metode GetGeneration .
DeadMG
2
.NET pada Windows menggunakan Generational Garbage Collector, namun .NET Compact Framework seperti yang digunakan pada WP7 dan Xbox360 memiliki GC yang lebih terbatas, walaupun itu mungkin lebih dari sekadar daftar datar, itu bukan yang generasional penuh.
Roy T.
Jonathan Dickinson: Saya ingin menambahkan bahwa struct tidak selalu jawabannya. Dalam. Net dan mungkin juga Mono sebuah struct tidak selalu hidup di stack (yang bagus) tetapi juga bisa hidup di heap (yang mana Anda membutuhkan seorang Kolektor Sampah). Aturan untuk ini cukup rumit tetapi secara umum itu terjadi ketika sebuah struct berisi tipe-tipe yang tidak bernilai.
Roy T.
1
@ Roy. lihat komentar di atas. Saya menunjukkan bahwa struct baik untuk sejumlah besar data dalam array - seperti sistem partikel. Jika Anda khawatir tentang struct GC dalam array adalah cara untuk pergi; untuk data seperti partikel.
Jonathan Dickinson
10
@DeadMG juga WTF sangat tidak pantas - mengingat Anda sebenarnya tidak membaca jawabannya dengan benar. Harap tenangkan diri Anda dan baca kembali jawabannya sebelum menulis komentar yang penuh kebencian di komunitas yang berperilaku baik seperti ini.
Jonathan Dickinson
7

Instansiasi dinamis sangat sedikit terjadi secara otomatis di dalam pangkalan perpustakaan, kecuali jika Anda memanggil sesuatu yang memerlukannya. Sebagian besar alokasi dan alokasi memori berasal dari kode Anda sendiri dan Anda dapat mengendalikannya.

Seperti yang saya pahami, Anda tidak dapat menghindari ini sepenuhnya - yang dapat Anda lakukan adalah memastikan bahwa Anda mendaur ulang dan menyatukan objek jika memungkinkan, Gunakan struct sebagai pengganti kelas, hindari sistem UnityGUI, dan umumnya hindari membuat objek baru dalam memori dinamis selama waktu ketika kinerja penting.

Anda juga dapat memaksa pengumpulan sampah pada waktu-waktu tertentu, yang mungkin atau mungkin tidak membantu: lihat beberapa saran di sini: http://unity3d.com/support/documentation/Manual/iphone-Optimizing-Scripts.html

Kylotan
sumber
2
Anda harus benar-benar menjelaskan implikasi memaksakan GC. Ini memainkan malapetaka dengan heuristik dan tata letak GC dan dapat menyebabkan masalah memori yang lebih besar jika digunakan secara tidak benar: komunitas XNA / MVPs / staf umumnya menganggap konten pasca-pemuatan merupakan satu-satunya waktu yang masuk akal untuk memaksa pengumpulan. Anda harus benar-benar menghindari menyebutkannya sepenuhnya jika Anda hanya mendedikasikan satu kalimat untuk masalah ini. GC.Collect()= bebaskan memori sekarang tetapi kemungkinan besar masalah nanti.
Jonathan Dickinson
Tidak, Anda harus menjelaskan implikasi pemaksaan GC karena Anda tahu lebih banyak tentangnya daripada saya. :) Namun, ingatlah bahwa Mono GC tidak harus sama dengan Microsoft GC, dan risikonya mungkin tidak sama.
Kylotan
Tetap, +1. Saya melanjutkan dan menguraikan beberapa pengalaman pribadi dengan GC - yang menurut Anda menarik.
Jonathan Dickinson