Bagaimana cara memanfaatkan cache cpu di mesin game sistem komponen entitas?

15

Saya sering membaca dalam dokumentasi mesin game ECS yang merupakan arsitektur yang baik untuk menggunakan cache cpu dengan bijak.

Tapi saya tidak tahu bagaimana kita bisa mendapat manfaat dari cpu cache.

Jika komponen disimpan dalam array (atau kumpulan), dalam memori yang berdekatan, itu adalah cara yang baik untuk menggunakan cache cpu TAPI hanya jika kita membaca komponen secara berurutan.

Ketika kita menggunakan sistem, mereka membutuhkan daftar entitas yang merupakan daftar entitas yang memiliki komponen dengan tipe tertentu.

Tetapi daftar ini memberikan komponen secara acak, tidak berurutan.

Jadi bagaimana merancang ECS ​​untuk memaksimalkan hit cache?

EDIT:

Sebagai contoh, sistem Fisika membutuhkan daftar entitas untuk entitas yang memiliki komponen RigidBody dan Transform (Ada kumpulan untuk RigidBody dan kumpulan untuk komponen Transform).

Jadi loop untuk memperbarui entitas akan seperti ini:

for (Entity eid in entitiesList) {
    // Get rigid body component
    RigidBody *rigidBody = entityManager.getComponentFromEntity<RigidBody>(eid);

    // Get transform component
    Transform *transform = entityManager.getComponentFromEntity<Transform>(eid);

    // Do something with rigid body and transform component
}

Masalahnya adalah bahwa komponen RigidBody dari entitas1 dapat berada di indeks 2 dari kumpulannya dan komponen Tranform dari entitas1 pada indeks 0 dari kumpulannya (karena beberapa entitas dapat memiliki beberapa komponen dan bukan yang lain dan karena menambah / menghapus entitas / komponen secara acak).

Jadi, bahkan jika komponen bersebelahan dalam memori, mereka dibaca secara acak sehingga akan memiliki lebih banyak cache miss, bukan?

Kecuali ada cara untuk mengambil komponen berikutnya dalam loop?

Johnmph
sumber
dapatkah Anda menunjukkan kepada kami bagaimana Anda mengalokasikan setiap komponen?
concept3d
Dengan pengalokasi kumpulan yang sederhana dan Handle manager untuk memiliki referensi komponen untuk mengelola relokasi komponen di kumpulan (untuk menjaga komponen yang bersebelahan dalam memori).
Johnmph
Contoh loop Anda menganggap bahwa pembaruan komponen disisipkan per entitas. Dalam banyak kasus dimungkinkan untuk memperbarui komponen secara massal berdasarkan tipe komponen (mis. Perbarui semua komponen benda tegar terlebih dahulu, kemudian perbarui semua transformasi dengan data benda tegar yang sudah selesai, kemudian perbarui semua rendering data dengan transformasi baru ...) - ini dapat meningkatkan cache gunakan untuk setiap pembaruan komponen. Saya pikir jenis struktur ini adalah apa yang disarankan Nick Wiggill di bawah ini.
DMGregory
Ini contoh saya yang buruk, sebenarnya, ini lebih merupakan sistem "perbarui semua transformasi dengan data tubuh yang sudah jadi" daripada sistem Fisika. Tetapi masalahnya tetap sama, dalam sistem ini (pembaruan transformasi dengan tubuh yang kaku, pembaruan rendering dengan transformasi, ...), kita perlu memiliki lebih dari satu jenis komponen secara bersamaan.
Johnmph
Tidak yakin apakah ini juga relevan? gamasutra.com/view/feature/6345/…
DMGregory

Jawaban:

13

Artikel Mick West menjelaskan proses linearisasi data komponen entitas, secara lengkap. Ini bekerja untuk seri Tony Hawk, bertahun-tahun lalu, pada perangkat keras yang jauh lebih mengesankan daripada yang kita miliki saat ini, untuk sangat meningkatkan kinerja. Dia pada dasarnya menggunakan global, pra-alokasi array untuk setiap tipe data entitas yang berbeda (posisi, skor dan yang lainnya) dan mereferensikan setiap array dalam fase berbeda dari update()fungsi seluruh sistemnya . Anda dapat mengasumsikan bahwa data untuk setiap entitas akan berada pada indeks array yang sama di setiap array global ini, jadi misalnya, jika pemain dibuat terlebih dahulu, mungkin datanya ada di [0]dalam setiap array.

Bahkan lebih spesifik untuk optimasi cache, slide Christer Ericsson untuk C dan C ++.

Untuk memberikan sedikit lebih detail, Anda harus mencoba menggunakan blok memori yang berdekatan (paling mudah dialokasikan sebagai array) per setiap jenis data (misalnya posisi, xy dan z), untuk memastikan lokalitas referensi yang baik, memanfaatkan setiap blok data tersebut secara berbeda. update()fase demi temporal locality yaitu untuk memastikan cache tidak memerah melalui algoritma LRU perangkat keras sebelum Anda menggunakan kembali data yang ingin Anda gunakan kembali, dalam update()panggilan yang diberikan . Seperti yang telah Anda katakan, apa yang tidak ingin Anda lakukan adalah mengalokasikan entitas dan komponen Anda sebagai objek diskrit melalui new, karena data dari berbagai jenis pada setiap instance entitas kemudian akan disatukan, mengurangi lokalitas referensi.

Jika Anda memiliki saling ketergantungan antara komponen (data) sehingga Anda benar-benar tidak mampu memisahkan data dari data terkait (mis. Transform + Fisika, Transform + Renderer) maka Anda dapat memilih untuk mereplikasi Transform data dalam array Fisika dan Renderer , memastikan bahwa semua data terkait sesuai dengan lebar garis cache untuk setiap operasi yang kritis-kinerja.

Ingat juga bahwa L2 dan L3 cache (jika Anda dapat menganggap ini untuk platform target Anda) melakukan banyak hal untuk meringankan masalah yang mungkin diderita oleh cache L1, seperti lebar garis yang terbatas. Jadi, bahkan pada L1 yang ketinggalan, ini adalah jaring pengaman yang paling sering mencegah pemanggilan ke memori utama, yang urutan besarnya lebih lambat dari pemanggilan ke tingkat cache apa pun.

Catatan tentang menulis data. Menulis tidak memanggil ke memori utama. Secara default, sistem saat ini telah mengaktifkan cache kembali : menulis nilai hanya menulisnya ke cache (awalnya), bukan ke memori utama, sehingga Anda tidak akan mengalami hambatan oleh ini. Hanya ketika data diminta dari memori utama (tidak akan terjadi ketika sedang dalam cache) dan basi, memori utama itu akan diperbarui dari cache.

Insinyur
sumber
1
Hanya sebuah catatan untuk siapa saja yang mungkin baru mengenal C ++: std::vectorpada dasarnya adalah array yang dapat diubah ukurannya secara dinamis dan karenanya juga berdekatan (secara de facto di versi C ++ yang lebih lama dan de jure dalam versi C ++ yang lebih baru). Beberapa implementasi std::dequejuga "cukup berdekatan" (meskipun bukan Microsoft).
Sean Middleditch
2
@Johnmph Cukup sederhana: Jika Anda tidak memiliki referensi lokal, Anda tidak punya apa-apa. Jika dua potong data terkait erat (seperti informasi spasial dan fisika), yaitu data tersebut diproses bersama, maka Anda mungkin harus memadatkannya sebagai satu komponen tunggal, disatukan. Tapi ingat kemudian bahwa setiap logika lain (misalnya, AI) yang leverage bahwa data spasial mungkin kemudian menderita sebagai akibat dari data spasial tidak disertakan bersama itu . Jadi itu tergantung pada apa yang paling membutuhkan kinerja (mungkin fisika dalam kasus Anda). Apakah itu masuk akal?
Insinyur
1
@ Johnmph ya, saya sepenuhnya setuju dengan Nick tentang bagaimana mereka disimpan dalam memori, jika Anda memiliki entitas dengan pointer ke dua komponen yang jauh di memori Anda tidak memiliki lokalitas, mereka harus sesuai dengan garis cache.
concept3d
2
@Johnmph: Memang, artikel Mick West mengasumsikan saling ketergantungan minimal. Jadi: Minimalkan ketergantungan; Data mereplikasi sepanjang garis Cache di mana Anda tidak dapat meminimalkan dependensi ... misalnya termasuk Transform bersama kedua benda tegar dan Render; dan agar sesuai dengan garis cache, Anda mungkin perlu mengurangi atom data sebanyak mungkin ... ini dapat dicapai sebagian dengan beralih dari titik mengambang ke titik tetap (4 byte vs 2 byte) per nilai poin desimal. Tetapi dengan satu atau lain cara, tidak peduli bagaimana Anda melakukannya, data Anda harus sesuai dengan lebar garis cache seperti dicatat concept3d, untuk kinerja maksimal.
Insinyur
2
@Johnmph. Tidak. Setiap kali Anda menulis Transform data, Anda cukup menulisnya untuk kedua array. Ini bukan tulisan yang perlu Anda khawatirkan. Setelah Anda mengirim surat, itu sama saja dengan selesai. Ini adalah membaca , nanti di update, ketika Anda menjalankan Fisika dan Renderer, yang harus memiliki akses ke semua data yang bersangkutan, segera, dalam baris cache tunggal kanan dekat dan pribadi ke CPU. Juga, jika Anda benar-benar membutuhkan semuanya bersama-sama, maka Anda dapat melakukan replikasi lebih lanjut atau Anda memastikan fisika, mengubah dan membuat sesuai satu baris cache ... 64 byte adalah umum dan sebenarnya cukup banyak data! ...
Insinyur