Berapa banyak utas yang harus saya miliki, dan untuk apa?

81

Haruskah saya memiliki utas terpisah untuk rendering dan logika, atau bahkan lebih?

Saya menyadari penurunan kinerja luar biasa yang disebabkan oleh sinkronisasi data (apalagi kunci mutex).

Saya telah berpikir untuk mengambil ini ke ekstrim dan melakukan utas untuk setiap subsistem yang mungkin. Tapi saya khawatir hal itu akan memperlambat juga. (Misalnya, apakah itu waras untuk memisahkan utas input dari render atau utas logika game?) Apakah sinkronisasi data yang diperlukan membuatnya tidak ada gunanya atau bahkan lebih lambat?

j riv
sumber
6
platform yang mana? PC, konsol NextGen, telepon pintar?
Ellis
Ada satu hal yang dapat saya pikirkan yang membutuhkan multi-threading; jaringan.
Soapy
keluar dari exagerations, tidak ada "besar" memperlambat ketika kunci terlibat. ini adalah legenda urban, dan prasangka.
v.oddou

Jawaban:

61

Pendekatan umum untuk mengambil keuntungan dari banyak core adalah, terus terang, hanya sesat. Memisahkan subsistem Anda menjadi utas yang berbeda memang akan membagi beberapa pekerjaan di beberapa inti, tetapi ia memiliki beberapa masalah besar. Pertama, sangat sulit untuk dikerjakan. Siapa yang mau bercanda dengan kunci dan sinkronisasi dan komunikasi dan hal-hal ketika mereka bisa saja menulis langsung rendering atau kode fisika saja? Kedua, pendekatan tersebut tidak benar-benar ditingkatkan. Paling-paling, ini akan memungkinkan Anda untuk mengambil keuntungan dari mungkin tiga atau empat core, dan itu jika Anda benar - benar tahu apa yang Anda lakukan. Hanya ada begitu banyak subsistem dalam sebuah gim, dan dari mereka bahkan ada lebih sedikit lagi yang memakan waktu CPU yang besar. Ada beberapa alternatif bagus yang saya tahu.

Salah satunya adalah memiliki utas utama bersama dengan utas pekerja untuk setiap CPU tambahan. Terlepas dari subsistem, utas utama mendelegasikan tugas yang terisolasi ke utas pekerja melalui semacam antrian; tugas-tugas ini sendiri dapat membuat tugas-tugas lain, juga. Satu-satunya tujuan utas pekerja adalah untuk masing-masing mengambil tugas dari antrian satu per satu dan menjalankannya. Namun, hal yang paling penting adalah bahwa segera setelah utas membutuhkan hasil tugas, jika tugas tersebut selesai, ia dapat memperoleh hasilnya, dan jika tidak, ia dapat dengan aman menghapus tugas dari antrian dan melanjutkan dan melakukan itu tugas itu sendiri. Artinya, tidak semua tugas pada akhirnya dijadwalkan secara paralel satu sama lain. Memiliki lebih banyak tugas daripada yang dapat dieksekusi secara paralel adalah baikhal dalam hal ini; itu berarti bahwa kemungkinan untuk skala ketika Anda menambahkan lebih banyak inti. Satu kelemahan untuk ini adalah bahwa itu membutuhkan banyak pekerjaan di muka untuk merancang antrian yang layak dan lingkaran pekerja kecuali Anda memiliki akses ke perpustakaan atau runtime bahasa yang sudah menyediakan ini untuk Anda. Bagian tersulit adalah memastikan tugas Anda benar-benar terisolasi dan aman, dan memastikan tugas Anda berada di jalan tengah yang menyenangkan antara yang berbutir kasar dan berbutir halus.

Alternatif lain untuk thread subsistem adalah memparalelkan setiap subsistem secara terpisah. Artinya, alih-alih menjalankan rendering dan fisika di utasnya sendiri, tulis subsistem fisika untuk menggunakan semua core Anda sekaligus, tulis subsistem rendering untuk menggunakan semua core Anda sekaligus, kemudian mintalah dua sistem berjalan secara berurutan (atau disisipkan, tergantung pada aspek lain dari arsitektur gim Anda). Misalnya, dalam subsistem fisika Anda bisa mengambil semua titik massa dalam permainan, membaginya di antara inti Anda, dan kemudian minta semua inti memperbaruinya sekaligus. Setiap inti kemudian dapat bekerja pada data Anda dalam loop ketat dengan lokasi yang baik. Gaya paralelisme kunci-langkah ini mirip dengan apa yang dilakukan GPU. Bagian tersulit di sini adalah memastikan bahwa Anda membagi pekerjaan Anda menjadi potongan-potongan halus sehingga membagi secara meratasebenarnya menghasilkan jumlah pekerjaan yang sama di semua prosesor.

Namun, kadang-kadang itu hanya termudah, karena politik, kode yang ada, atau keadaan frustasi lainnya, untuk memberikan setiap subsistem sebuah utas. Dalam hal ini, yang terbaik adalah menghindari membuat lebih banyak thread OS daripada core untuk beban kerja CPU yang berat (jika Anda memiliki runtime dengan thread ringan yang kebetulan menyeimbangkan seluruh core Anda, ini bukan masalah besar). Selain itu, hindari komunikasi yang berlebihan. Salah satu trik yang bagus adalah mencoba pipelining; setiap subsistem utama dapat bekerja pada kondisi permainan yang berbeda pada suatu waktu. Pipelining mengurangi jumlah komunikasi yang diperlukan di antara subsistem Anda karena mereka tidak semua memerlukan akses ke data yang sama pada saat yang sama, dan itu juga dapat menghapuskan beberapa kerusakan yang disebabkan oleh kemacetan. Sebagai contoh, jika subsistem fisika Anda cenderung membutuhkan waktu yang lama untuk diselesaikan dan subsistem rendering Anda berakhir selalu menunggunya, laju bingkai absolut Anda bisa lebih tinggi jika Anda menjalankan subsistem fisika untuk frame berikutnya sementara subsistem rendering masih bekerja pada sebelumnya bingkai. Bahkan, jika Anda memiliki hambatan seperti itu dan tidak dapat menghapusnya dengan cara lain, pipelining mungkin merupakan alasan paling sah untuk repot dengan utas subsistem.

Jake McArthur
sumber
"segera setelah utas membutuhkan hasil dari suatu tugas, jika tugas itu selesai ia dapat memperoleh hasilnya, dan jika tidak, ia dapat dengan aman menghapus tugas dari antrian dan melanjutkan dan melakukan tugas itu sendiri". Apakah Anda berbicara tentang tugas yang dihasilkan oleh utas yang sama? Jika demikian, bukankah lebih masuk akal jika tugas itu dieksekusi oleh utas yang memunculkan tugas itu sendiri?
jmp97
yaitu utas bisa, tanpa menjadwalkan tugas, segera jalankan tugas itu.
jmp97
3
Intinya adalah bahwa utas tidak selalu tahu di muka apakah akan lebih baik untuk menjalankan tugas secara paralel atau tidak. Idenya adalah untuk secara spekulatif memicu pekerjaan yang pada akhirnya perlu Anda lakukan, dan jika utas lain menemukan dirinya menganggur maka ia dapat melanjutkan dan melakukan pekerjaan ini untuk Anda. Jika ini akhirnya tidak terjadi pada saat Anda membutuhkan hasilnya, Anda bisa menarik sendiri tugas dari antrian. Skema ini untuk menyeimbangkan beban kerja secara dinamis di beberapa inti daripada secara statis.
Jake McArthur
Maaf sudah terlalu lama untuk kembali ke utas ini. Saya tidak memperhatikan gamedev belakangan ini. Ini mungkin jawaban terbaik, terus terang tetapi langsung dan luas.
j riv
1
Anda benar dalam arti bahwa saya lalai berbicara tentang beban kerja saya / berat. Interpretasi saya terhadap pertanyaan adalah bahwa itu hanya tentang beban kerja yang berat pada CPU.
Jake McArthur
30

Ada beberapa hal yang perlu dipertimbangkan. Rute thread-per-subsistem mudah dipikirkan karena pemisahan kode cukup jelas dari awal. Namun, tergantung pada seberapa banyak interkomunikasi yang dibutuhkan subsistem Anda, komunikasi antar-thread dapat benar-benar mematikan kinerja Anda. Selain itu, ini hanya menskala ke inti N, di mana N adalah jumlah subsistem yang Anda abstraksi menjadi utas.

Jika Anda hanya ingin multithread gim yang ada, ini mungkin jalan perlawanan paling sedikit. Namun, jika Anda bekerja pada beberapa sistem mesin level rendah yang mungkin dibagi antara beberapa game atau proyek, saya akan mempertimbangkan pendekatan lain.

Mungkin perlu sedikit pemikiran, tetapi jika Anda dapat memecahnya sebagai antrian pekerjaan dengan serangkaian utas pekerja, ini akan menjadi skala yang jauh lebih baik dalam jangka panjang. Saat chip terbaru dan terhebat keluar dengan trilyun core, kinerja game Anda akan meningkat bersamanya, cukup jalankan lebih banyak thread pekerja.

Jadi pada dasarnya, jika Anda ingin mengaitkan beberapa paralelisme dengan proyek yang ada, saya akan memparalelkan antar subsistem. Jika Anda sedang membangun mesin baru dari awal dengan skalabilitas paralel dalam pikiran, saya akan melihat ke dalam antrian pekerjaan.

Bob Somers
sumber
Sistem yang Anda sebutkan sangat mirip dengan sistem penjadwalan yang disebutkan dalam jawaban yang diberikan oleh James Lainnya, detail yang masih bagus di area itu jadi +1 karena menambah diskusi.
James
3
sebuah wiki komunitas tentang cara mengatur antrian pekerjaan dan utas pekerja akan menyenangkan.
bot_bot
23

Pertanyaan itu tidak memiliki jawaban terbaik, karena itu tergantung pada apa yang ingin Anda capai.

Xbox memiliki tiga inti dan dapat menangani beberapa utas sebelum konteks mengalihkan overhead menjadi masalah. PC dapat menangani beberapa lagi.

Banyak game yang biasanya single threaded untuk kemudahan pemrograman. Ini bagus untuk sebagian besar permainan pribadi. Satu-satunya hal yang Anda mungkin harus memiliki utas lainnya adalah Jaringan dan Audio.

Unreal memiliki utas permainan, utas render, utas jaringan, dan utas audio (jika saya ingat dengan benar). Ini cukup standar untuk banyak mesin gen saat ini, meskipun dapat mendukung thread rendering terpisah dapat menyebalkan dan melibatkan banyak pekerjaan dasar.

Mesin idTech5 yang sedang dikembangkan untuk Rage sebenarnya menggunakan sejumlah utas, dan ia melakukannya dengan memecah tugas game menjadi 'pekerjaan' yang diproses dengan sistem penugasan. Tujuan eksplisit mereka adalah untuk memiliki skala mesin permainan mereka dengan baik ketika jumlah core pada sistem game rata-rata melonjak.

Teknologi yang saya gunakan (dan telah ditulis) memiliki utas terpisah untuk Networking, Input, Audio, Rendering, dan Penjadwalan. Kemudian memiliki sejumlah utas yang dapat digunakan untuk melakukan tugas-tugas gim, dan ini dikelola oleh utas penjadwalan. Sebuah banyak pekerjaan yang masuk ke dalam mendapatkan semua benang untuk bermain baik dengan satu sama lain, tetapi tampaknya bekerja dengan baik dan mendapatkan sangat baik digunakan keluar sistem multicore, jadi mungkin itu adalah misi dicapai (untuk saat ini, saya mungkin memecah audio / networking / masukan pekerjaan hanya menjadi 'tugas' yang dapat diperbarui utas pekerja).

Itu sangat tergantung pada tujuan akhir Anda.

James
sumber
+1 untuk menyebutkan sistem Penjadwalan .. biasanya tempat yang bagus untuk pusat utas / sistem komunikasi :)
James
Mengapa memilih, downvoter?
jcora
12

Utas per subsistem adalah cara yang salah. Tiba-tiba, aplikasi Anda tidak akan skala karena beberapa subsistem menuntut lebih banyak daripada yang lain. Ini adalah pendekatan threading yang diambil oleh Panglima Tertinggi dan tidak berskala melebihi dua inti karena mereka hanya memiliki dua subsistem yang mengambil jumlah substansial dari rendering CPU dan logika fisika / permainan, meskipun mereka memiliki 16 utas, utas lainnya hampir tidak berarti pekerjaan apa pun dan sebagai hasilnya, permainan hanya ditingkatkan menjadi dua core.

Yang harus Anda lakukan adalah menggunakan sesuatu yang disebut kolam ulir. Ini agak mencerminkan pendekatan yang diambil pada GPU - yaitu, Anda memposting pekerjaan, dan setiap utas yang tersedia muncul begitu saja dan melakukannya, dan kemudian kembali untuk menunggu pekerjaan - pikirkan itu seperti buffer cincin, dari utas. Pendekatan ini memiliki keunggulan penskalaan N-core dan sangat baik dalam penskalaan untuk jumlah inti rendah dan tinggi. Kerugiannya adalah cukup sulit untuk mengerjakan kepemilikan utas untuk pendekatan ini, karena tidak mungkin mengetahui utas mana yang melakukan pekerjaan pada waktu tertentu, jadi Anda harus memiliki masalah kepemilikan yang dikunci dengan sangat ketat. Ini juga membuatnya sangat sulit untuk menggunakan teknologi seperti Direct3D9 yang tidak mendukung banyak utas.

Thread pool sangat sulit digunakan, tetapi memberikan hasil terbaik. Jika Anda membutuhkan penskalaan yang sangat bagus, atau Anda punya banyak waktu untuk mengerjakannya, gunakan kumpulan utas. Jika Anda mencoba memperkenalkan paralelisme ke dalam proyek yang ada dengan masalah ketergantungan yang tidak diketahui dan teknologi single-threaded, ini bukan solusi untuk Anda.

DeadMG
sumber
Untuk lebih tepatnya: GPU tidak menggunakan thread pools melainkan scheduler thread diimplementasikan dalam perangkat keras, yang membuatnya sangat murah untuk membuat utas baru dan mengganti utas, berbeda dengan CPU di mana pembuatan utas dan sakelar konteks mahal. Lihat Panduan Programmer Nvidias CUDA misalnya.
Nils
2
+1: Jawaban terbaik di sini. Saya bahkan akan menggunakan konstruksi yang lebih abstrak daripada threadpools (misalnya antrian pekerjaan dan pekerja) jika kerangka kerja Anda memungkinkannya. Jauh lebih mudah untuk berpikir / memprogram dalam istilah ini daripada di utas murni / kunci / dll. Plus: Memecah permainan Anda dalam rendering, logika, dll. Adalah omong kosong, karena rendering harus menunggu hingga logika selesai. Alih-alih menciptakan pekerjaan yang sebenarnya dapat dieksekusi secara paralel (misalnya: Hitung AI untuk satu npc untuk frame berikutnya).
Dave O.
@DO. Poin "Plus" Anda sangat, sangat benar.
Insinyur
11

Anda benar bahwa bagian yang paling penting adalah menghindari sinkronisasi sedapat mungkin. Ada beberapa cara untuk mencapai ini.

  1. Ketahui data Anda dan simpan dalam memori sesuai dengan kebutuhan pemrosesan Anda. Ini memungkinkan Anda merencanakan perhitungan paralel tanpa perlu sinkronisasi. Sayangnya ini adalah sebagian besar waktu yang cukup sulit untuk dicapai karena data sering diakses dari sistem yang berbeda pada waktu yang tidak dapat diprediksi.

  2. Tentukan waktu akses yang jelas untuk data. Anda dapat memisahkan centang-utama Anda menjadi x fase. Jika Anda yakin bahwa Thread X membaca data hanya dalam fase tertentu, Anda juga tahu bahwa data ini dapat dimodifikasi oleh utas lain dalam fase yang berbeda.

  3. Gandakan data Anda. Itu adalah pendekatan yang paling sederhana, tetapi meningkatkan latensi, karena Thread X bekerja dengan data dari frame terakhir, sementara Thread Y sedang mempersiapkan data untuk frame berikutnya.

Pengalaman pribadi saya menunjukkan bahwa perhitungan berbutir halus adalah cara yang paling efektif, karena ini dapat berskala jauh lebih baik daripada solusi berbasis subsistem. Jika Anda merangkai subsistem Anda, frame-time akan terikat pada subsistem yang paling mahal. Ini dapat menyebabkan semua utas tetapi satu idle sampai subsistem mahal akhirnya selesai bekerja. Jika Anda dapat memisahkan sebagian besar permainan Anda menjadi tugas-tugas kecil, tugas-tugas ini dapat dijadwalkan sesuai untuk menghindari core idle. Tetapi ini adalah sesuatu yang sulit dicapai jika Anda sudah memiliki basis kode yang besar.

Untuk mempertimbangkan beberapa kendala perangkat keras, Anda harus mencoba untuk tidak pernah terlalu banyak berlangganan perangkat keras Anda. Dengan kelebihan langganan, maksud saya memiliki lebih banyak utas perangkat lunak daripada utas perangkat keras platform Anda. Terutama pada arsitektur PPC (Xbox360, PS3), tugas-switch sangat mahal. Ini tentu saja sangat oke jika Anda memiliki beberapa thread kelebihan permintaan yang hanya dipicu untuk sejumlah kecil waktu (sekali bingkai, misalnya) Jika Anda menargetkan PC, Anda harus ingat bahwa jumlah core (atau lebih baik HW) -Threads) terus berkembang, jadi Anda ingin mencari solusi yang skalabel, yang memanfaatkan CPU-Power tambahan. Jadi, di area ini, Anda harus mencoba mendesain kode berdasarkan tugas.

DarthCoder
sumber
3

Aturan umum praktis untuk memasukkan aplikasi: 1 utas per CPU Core. Pada quad core PC yang berarti 4. Seperti telah dicatat, XBox 360 memiliki 3 core tetapi 2 thread hardware masing-masing, jadi 6 thread dalam hal ini. Pada sistem seperti PS3 ... semoga sukses untuk yang satu itu :) Orang-orang masih berusaha mencari tahu.

Saya akan menyarankan merancang setiap sistem sebagai modul mandiri yang dapat Anda utas jika Anda mau. Ini biasanya berarti memiliki jalur komunikasi yang sangat jelas antara modul dan bagian mesin yang lain. Saya sangat suka proses Read-Only seperti Rendering dan audio serta proses 'apakah kita sudah sampai' seperti membaca input pemain untuk hal-hal yang dapat di-threaded. Untuk menyentuh jawaban yang diberikan oleh AttackingHobo, ketika Anda membuat 30-60fps, jika data Anda 1 / 30th-1 / 60th out of date, itu benar-benar tidak akan mengurangi rasa responsif dari permainan Anda. Selalu ingat bahwa perbedaan utama antara perangkat lunak aplikasi dan permainan video adalah melakukan segalanya 30-60 kali per detik. Namun pada catatan yang sama,

Jika Anda mendesain sistem engine Anda dengan cukup baik, salah satunya dapat dipindahkan dari thread ke thread untuk memuat keseimbangan mesin Anda lebih tepat pada basis per-game dan sejenisnya. Secara teori Anda juga bisa menggunakan mesin Anda dalam sistem terdistribusi jika perlu di mana sistem komputer yang sepenuhnya terpisah menjalankan setiap komponen.

James
sumber
2
Xbox360 memiliki 2 hardwarethreads per core, sehingga jumlah utas yang optimal adalah 6.
DarthCoder
Ah, +1 :) Saya selalu terbatas pada area jaringan 360 dan ps3, hehe :)
James
0

Saya membuat satu utas per inti logis (minus satu, untuk menjelaskan Utas Utama, yang secara kebetulan bertanggung jawab atas Rendering, tetapi jika tidak bertindak sebagai Utas Pekerja juga).

Saya mengumpulkan peristiwa perangkat input secara waktu nyata di seluruh bingkai, tetapi tidak menerapkannya sampai akhir bingkai: mereka akan berpengaruh di bingkai berikutnya. Dan saya menggunakan logika yang sama untuk rendering (kondisi lama) versus memperbarui (kondisi baru).

Saya menggunakan acara atom untuk menunda operasi yang tidak aman sampai nanti dalam bingkai yang sama, dan saya menggunakan lebih dari satu antrian acara (job queue) untuk menerapkan penghalang memori yang memberikan jaminan berbalut besi mengenai urutan operasi, tanpa mengunci atau menunggu (mengunci antrian konkuren gratis dalam urutan prioritas pekerjaan).

Perlu dicatat bahwa pekerjaan apa pun dapat mengeluarkan subjobs (yang lebih halus, dan mendekati atomicity) ke antrian prioritas yang sama atau yang lebih tinggi (dilayani kemudian dalam bingkai).

Mengingat saya memiliki tiga antrian seperti itu, semua utas kecuali satu berpotensi dapat menunda tepat tiga kali per frame (sambil menunggu utas lainnya menyelesaikan semua pekerjaan luar biasa yang dikeluarkan pada tingkat prioritas saat ini).

Tampaknya ini adalah tingkat ketidakaktifan utas yang dapat diterima!

Homer
sumber
Bingkai saya dimulai dengan MAIN merender LAMA NEGARA dari pass pembaruan frame sebelumnya, sementara semua utas lainnya segera mulai menghitung status frame BERIKUTNYA, saya hanya menggunakan Acara untuk menggandakan perubahan status buffer hingga titik di frame di mana tidak ada yang membaca lagi .
Homer
0

Saya biasanya menggunakan satu utas utama (jelas) dan saya akan menambahkan utas setiap kali saya melihat penurunan kinerja sekitar 10 hingga 20 persen. Untuk menghilangkan drop seperti itu saya menggunakan alat kinerja studio visual. Peristiwa umum adalah (un) memuat beberapa area peta atau membuat beberapa perhitungan berat.

Lenard Arquin
sumber