Stack DOTS: C ++ & C #

gambar

Ini adalah pengantar singkat untuk Stack Teknologi Berorientasi Data ( DOTS ) kami yang baru. Kami akan membagikan beberapa wawasan untuk membantu Anda memahami bagaimana dan mengapa Unity menjadi seperti itu hari ini, dan juga memberi tahu Anda arah mana yang kami rencanakan untuk dikembangkan. Di masa depan, kami berencana untuk menerbitkan artikel baru di blog DOTS di blog Unity.

Mari kita bicara tentang C ++. Ini adalah bahasa di mana Kesatuan modern ditulis.
Salah satu masalah paling rumit yang harus dihadapi pengembang game dengan satu cara atau yang lain adalah: programmer harus memberikan file yang dapat dieksekusi dengan instruksi yang jelas untuk prosesor target, dan ketika prosesor menjalankan instruksi ini, permainan harus dimulai.

Pada bagian dari kode yang peka terhadap kinerja, kita tahu sebelumnya apa instruksi final yang seharusnya. Kita hanya perlu cara sederhana yang memungkinkan kita untuk secara konsisten menggambarkan logika kita, dan kemudian memeriksa dan memastikan bahwa instruksi yang kita butuhkan dihasilkan.

Kami percaya bahwa bahasa C ++ tidak terlalu bagus untuk tugas ini. Sebagai contoh, saya ingin loop saya menjadi vektor, tetapi mungkin ada sejuta alasan mengapa kompiler tidak akan dapat melakukan vektorisasi. Entah hari ini sedang vektor, dan besok tidak, karena beberapa perubahan yang tampaknya sepele. Sulit untuk memastikan bahwa semua kompiler C / C ++ saya bahkan akan membuat vektor kode saya.

Kami memutuskan untuk mengembangkan "cara yang cukup nyaman untuk menghasilkan kode mesin" kami sendiri yang akan memenuhi semua keinginan kami. Mungkin akan menghabiskan banyak waktu untuk sedikit membengkokkan seluruh rangkaian desain C ++ ke arah yang kita butuhkan, tetapi kami memutuskan bahwa akan jauh lebih masuk akal untuk menginvestasikan kekuatan kami dalam mengembangkan rantai alat yang akan sepenuhnya menyelesaikan semua masalah desain yang dihadapi kami. Kami akan mengembangkannya dengan mempertimbangkan tugas-tugas yang harus diselesaikan pengembang game.

Faktor apa yang kami prioritaskan?

  • Performa = benar. Saya harus bisa mengatakan: "jika karena alasan tertentu loop ini tidak di-vectorized, maka itu harus merupakan kesalahan kompiler, dan bukan situasi dari kategori" oh, kode mulai bekerja hanya delapan kali lebih lambat, tetapi masih memberikan nilai-nilai sejati, bisnis sesuatu! "
  • Lintas platform. Kode input yang saya tulis harus tetap persis sama terlepas dari platform target - baik itu iOS atau Xbox.
  • Kita harus memiliki loop iterasi yang rapi di mana saya dapat dengan mudah melihat kode mesin yang dihasilkan untuk arsitektur apa pun ketika saya mengubah kode sumber saya. "Penampil" kode mesin harus sangat membantu pelatihan / penjelasan ketika Anda perlu memahami apa yang dilakukan semua instruksi mesin ini.
  • Keamanan Sebagai aturan, pengembang game tidak menempatkan keselamatan pada posisi tinggi dalam daftar prioritas mereka, tetapi kami percaya bahwa salah satu fitur paling keren dari Unity adalah sangat sulit untuk merusak memori di dalamnya. Seharusnya ada mode seperti itu di mana kita menjalankan kode apa pun - dan kita jelas memperbaiki kesalahan dengan mana surat besar menampilkan pesan tentang apa yang terjadi di sini: misalnya, saya melampaui batas ketika membaca / menulis atau mencoba dereferensi nol.

Jadi, setelah mengetahui apa yang penting bagi kita, mari kita beralih ke pertanyaan berikutnya: dalam bahasa apa lebih baik menulis program dari mana kode mesin tersebut kemudian akan dihasilkan? Katakanlah kita memiliki opsi berikut:

  • Bahasa sendiri
  • Beberapa adaptasi / subset dari C atau C ++
  • Subset dari c #

Apa, C #? Untuk putaran batin kita yang kinerjanya sangat penting? Ya C # adalah pilihan yang sepenuhnya alami, yang dalam konteks Unity ada banyak hal yang sangat menyenangkan:

  • Ini adalah bahasa yang sudah digunakan pengguna kami hari ini.
  • Ini memiliki IDE yang sangat baik, baik untuk pengeditan / refactoring, dan untuk debugging.
  • Sudah ada kompiler yang mengubah C # ke IL perantara (kita berbicara tentang kompiler Roslyn untuk C # dari Microsoft), dan Anda cukup menggunakannya daripada menulis sendiri. Kami memiliki pengalaman yang kaya dalam mengubah bahasa perantara menjadi IL, jadi kami hanya perlu melakukan pembuatan kode dan memposting proses program tertentu.
  • C # tidak memiliki banyak masalah C ++ (neraka dengan dimasukkannya header, pola PIMPL, waktu kompilasi yang panjang)

Saya sendiri sangat suka menulis kode dalam C #. Namun, bahasa C # tradisional bukan bahasa terbaik dalam hal kinerja. Tim pengembangan C #, tim-tim yang bertanggung jawab atas perpustakaan standar dan runtime selama beberapa tahun terakhir telah membuat kemajuan luar biasa di bidang ini. Namun, saat bekerja dengan C #, tidak mungkin untuk mengontrol dengan tepat di mana data Anda berada di memori. Dan justru masalah inilah yang harus kita pecahkan untuk meningkatkan produktivitas.

Selain itu, perpustakaan standar bahasa ini disusun di sekitar "objek di heap" dan "objek yang memiliki petunjuk ke objek lain".

Pada saat yang sama, bekerja dengan fragmen kode di mana kinerja sangat penting, Anda hampir dapat sepenuhnya melakukannya tanpa perpustakaan standar (selamat tinggal pada Linq, StringFormatter, Daftar, Kamus), melarang operasi pemilihan (= tidak ada kelas, hanya struktur), refleksi, nonaktifkan pengumpul sampah dan virtual panggilan, dan menambahkan beberapa wadah baru yang diizinkan untuk digunakan (NativeArray dan perusahaan). Dalam hal ini, elemen yang tersisa dari bahasa C # sudah terlihat sangat bagus. Lihat blog Aras untuk contoh, di mana ia menggambarkan proyek pelacak jalur darurat.

Subset seperti itu akan membantu kita dengan mudah mengatasi semua tugas yang relevan saat bekerja dengan siklus panas. Karena ini adalah bagian lengkap dari C #, Anda dapat bekerja dengannya seperti dengan C # biasa. Kami dapat menerima kesalahan yang terkait dengan pergi ke luar negeri ketika mencoba mengakses, kami akan mendapatkan pesan kesalahan yang sangat baik, kami akan memiliki debugger yang didukung, dan kecepatan kompilasi akan sedemikian rupa sehingga Anda sudah lupa tentang bekerja dengan C ++. Kami sering menyebut subset ini sebagai High Performance C # atau HPC #.

Burst compiler: hari ini apa?


Kami menulis pembuat kode / kompiler yang disebut Burst. Ini tersedia dalam versi Unity 2018.1 dan lebih tinggi sebagai paket dalam mode "pratinjau". Masih banyak pekerjaan yang harus dilakukan dengannya, tetapi kami senang dengannya hari ini.

Terkadang kami berhasil bekerja lebih cepat daripada di C ++, sering - masih lebih lambat dari pada di C ++. Kategori kedua termasuk bug kinerja, yang, kami yakin, akan dapat mengatasinya.

Namun, membandingkan kinerja saja tidak cukup. Yang tidak kalah penting adalah apa yang perlu dilakukan untuk mencapai kinerja tersebut. Contoh: kami mengambil kode culling dari penyaji C ++ kami saat ini dan memindahkannya ke Burst. Performa tidak berubah, tetapi dalam versi C ++ kami harus melakukan tindakan penyeimbangan yang luar biasa untuk meyakinkan kompiler C ++ kami untuk melakukan vektorisasi. Versi dengan Burst sekitar empat kali lebih kompak.

Jujur, keseluruhan cerita dengan "Anda harus menulis ulang kode Anda penting untuk kinerja dalam C #" pada pandangan pertama tidak menarik bagi siapa pun di tim Persatuan internal. Bagi kebanyakan dari kita, itu terdengar seperti "lebih dekat ke perangkat keras!" Ketika bekerja dengan C ++. Tetapi sekarang situasinya telah berubah. Dengan menggunakan C #, kami sepenuhnya mengendalikan seluruh proses mulai dari kompilasi kode sumber hingga menghasilkan kode mesin, dan jika kami tidak menyukai detail apa pun, kami hanya mengambil dan memperbaikinya.

Kita akan mem-porting secara perlahan tapi pasti semua kode kinerja-kritis dari C ++ ke HPC #. Dalam bahasa ini, lebih mudah untuk mencapai kinerja yang kita butuhkan, lebih sulit untuk menulis bug, dan lebih mudah untuk dikerjakan.

Berikut adalah tangkapan layar Burst Inspector, tempat Anda dapat dengan mudah melihat instruksi perakitan mana yang dihasilkan untuk berbagai loop panas Anda:

gambar

Unity memiliki banyak pengguna yang berbeda. Beberapa dari mereka dapat mengingat seluruh rangkaian instruksi arm64 dari memori, sementara yang lain hanya membuat dengan antusias, bahkan tanpa gelar PhD dalam ilmu komputer.
Semua pengguna menang ketika mempercepat fraksi waktu bingkai yang dihabiskan untuk mengeksekusi kode mesin (biasanya 90% +). Bagian bekerja dengan kode yang dapat dieksekusi dari paket Toko Aset benar-benar semakin cepat, karena penulis paket Toko Aset mengadopsi HPC #.

Pengguna mahir juga akan mendapat manfaat dari fakta bahwa mereka dapat menulis kode kinerja tinggi mereka sendiri di HPC #.

Optimalisasi poin


Dalam C ++, sangat sulit untuk mendapatkan kompiler untuk membuat keputusan kompromi yang berbeda dalam mengoptimalkan kode di berbagai bagian proyek Anda. Optimasi yang paling terperinci yang dapat Anda andalkan adalah indikasi file-per-file dari tingkat optimasi.

Burst dirancang sehingga Anda dapat menerima satu-satunya metode program ini sebagai input, yaitu: titik masuk ke loop panas. Burst mengkompilasi fungsi ini, serta semua yang dipanggil (elemen yang dipanggil harus dijamin diketahui sebelumnya: kami tidak mengizinkan fungsi virtual atau pointer fungsi).

Karena Burst beroperasi hanya pada sebagian kecil dari program, kami menetapkan tingkat optimisasi menjadi 11. Burst menyematkan hampir setiap situs panggilan. Hapus if-check, yang sebaliknya tidak akan dihapus, karena dalam formulir yang disematkan kami mendapatkan informasi yang lebih lengkap tentang argumen fungsi.

Bagaimana Ini Membantu Memecahkan Masalah Umum Threading?


C ++ (dan juga C #) tidak secara khusus membantu pengembang menulis kode aman.

Bahkan hari ini, lebih dari satu dekade setelah prosesor game tipikal mulai dilengkapi dengan dua atau lebih core, sangat sulit untuk menulis program yang secara efisien menggunakan beberapa core.

Balap data, non-determinisme, dan kebuntuan adalah tantangan utama yang membuatnya sangat sulit untuk menulis kode multi-utas. Dalam konteks ini, kita memerlukan fitur dari kategori "pastikan bahwa fungsi ini dan semua yang dipanggilnya tidak akan pernah mulai membaca atau menulis keadaan global." Kami ingin semua pelanggaran aturan ini memberikan kesalahan pada kompiler, dan tidak tetap "aturan yang kami harap semua programmer patuhi." Burst melempar kesalahan kompilasi.

Kami sangat menyarankan agar pengguna Unity (dan kami tetap sama di lingkaran mereka) menulis kode sehingga semua transformasi data yang direncanakan di dalamnya dibagi menjadi beberapa tugas. Setiap tugas "fungsional", dan, sebagai efek samping, gratis. Ini secara eksplisit menunjukkan buffer hanya-baca dan buffer baca / tulis yang harus digunakan. Upaya apa pun untuk mengakses data lain akan menyebabkan kesalahan kompilasi.
Penjadwal Tugas memastikan bahwa tidak ada yang akan menulis ke buffer baca-saja saat tugas Anda sedang berjalan. Dan kami menjamin bahwa selama durasi tugas tidak ada yang akan membaca dari buffer Anda, yang dirancang untuk membaca dan menulis.

Setiap kali Anda menetapkan tugas yang melanggar aturan ini, Anda akan menerima kesalahan kompilasi. Bukan hanya di acara yang disayangkan seperti kondisi lomba. Pesan kesalahan akan menjelaskan bahwa Anda mencoba untuk menetapkan tugas yang seharusnya dibaca dari buffer A, tetapi sebelumnya Anda telah menetapkan tugas yang akan menulis ke A. Oleh karena itu, jika Anda benar-benar ingin melakukan ini, maka tugas sebelumnya harus ditentukan sebagai ketergantungan. .

Kami percaya bahwa mekanisme keamanan seperti itu membantu menangkap banyak bug sebelum diperbaiki, dan karenanya memastikan efisiensi penggunaan semua core. Menjadi tidak mungkin untuk memprovokasi kondisi balapan atau kebuntuan. Hasil dijamin pasti deterministik, terlepas dari berapa banyak utas yang Anda miliki, atau berapa kali utas terputus karena intervensi dari beberapa proses lainnya.

Kuasai seluruh tumpukan


Ketika kita dapat mencapai bagian bawah dari semua komponen ini, kita juga dapat memastikan bahwa mereka saling menyadari. Sebagai contoh, alasan umum untuk kegagalan vektorisasi adalah ini: kompiler tidak dapat menjamin bahwa dua pointer tidak akan menunjuk ke titik memori yang sama (aliasing). Kita tahu bahwa dua NativeArray tidak akan tumpang tindih seperti ini, karena mereka telah menulis perpustakaan koleksi, dan kita dapat menggunakan pengetahuan ini di Burst, jadi kita tidak akan menolak untuk mengoptimalkan hanya karena takut bahwa dua petunjuk mungkin diarahkan ke satu memori yang sama.

Demikian pula, kami menulis perpustakaan matematika Unity.Mathematics . Burst dia dikenal "menyeluruh" Burst (di masa depan) akan dapat menunjukkan memilih keluar dari optimasi dalam kasus-kasus seperti math.sin (). Karena untuk Burst math.sin () bukan hanya metode C # biasa yang perlu dikompilasi, ia juga akan memahami sifat trigonometrik dari dosa (), ia akan memahami bahwa sin (x) == x untuk nilai x kecil (yang dapat dibuktikan secara independen oleh Burst ), akan mengerti bahwa itu dapat digantikan oleh ekspansi dalam seri Taylor, sebagian mengorbankan akurasi. Di masa mendatang, Burst juga berencana untuk menerapkan lintas platform dan desain determinisme dengan titik mengambang - kami percaya bahwa tujuan tersebut dapat dicapai.

Perbedaan antara kode mesin gim dan kode gim kabur


Saat kami menulis kode runtime Unity di HPC #, mesin game dan gim tersebut semuanya ditulis dalam bahasa yang sama. Kami dapat mendistribusikan sistem runtime yang kami konversi ke HPC # sebagai kode sumber. Setiap orang dapat belajar dari mereka, meningkatkannya, menyesuaikannya untuk diri mereka sendiri. Kami akan memiliki bidang permainan pada tingkat tertentu, dan tidak ada yang akan mencegah pengguna kami menulis sistem partikel, fisika game, atau renderer yang lebih baik dari yang kami tulis. Dengan mendekatkan proses pengembangan internal kami ke proses pengembangan pengguna, kami juga dapat merasa lebih baik dalam posisi pengguna, jadi kami akan mengerahkan semua upaya kami untuk membangun alur kerja tunggal, dan bukan dua yang berbeda.

Source: https://habr.com/ru/post/id444526/


All Articles