Скільки ниток у мене повинно бути, і для чого?


81

Чи повинен я мати окремі потоки для візуалізації та логіки чи навіть більше?

Мені відомо про величезний спад продуктивності, спричинений синхронізацією даних (не кажучи вже про будь-які блокування мютексу).

Я думав зробити це до кінця і робити нитки, щоб зрозуміти кожну можливу підсистему. Але я переживаю, що теж може уповільнити справи. (Наприклад, чи розумно відокремити вхідний потік від потоків візуалізації чи логіки гри?) Чи потрібна синхронізація даних зробить його безглуздим чи навіть повільнішим?


6
яка платформа? ПК, NextGen консоль, смартфони?
Елліс

Я можу подумати про одне, що вимагало б багаторізкових ниток; мережа.
Soapy

киньте перебільшення, не існує "величезного" уповільнення, коли задіяні замки. це і міська легенда, і забобони.
v.oddou

Відповіді:


61

Загальний підхід до використання декількох ядер, відверто кажучи, просто неправдивий. Поділ ваших підсистем на різні потоки дійсно розділить частину роботи на декілька ядер, але це має деякі основні проблеми. По-перше, з цим дуже важко працювати. Хто хоче розібратися із замками та синхронізацією, спілкуванням та іншим предметом, коли натомість він може просто писати прямий візуалізацію чи фізичний код? По-друге, підхід насправді не збільшує масштаб. У кращому випадку це дозволить вам скористатися, можливо, трьома-чотирма ядрами, і це якщо ви дійсно знаєте, що робите. У грі є лише стільки підсистем, а таких ще менше, що займають великі шматки часу процесора. Є кілька хороших альтернатив, які я знаю.

Один - мати основний потік разом з робочою ниткою для кожного додаткового процесора. Незалежно від підсистеми, основний потік делегує окремі завдання робочим потокам через якусь чергу (и); ці завдання також можуть створювати ще й інші завдання. Єдине призначення робочих ниток - це кожне захоплення завдань із черги по черзі та їх виконання. Найголовніше, однак, що як тільки нитка потребує результату завдання, якщо завдання виконано, воно може отримати результат, а якщо ні, то можна сміливо видалити завдання з черги та йти вперед і виконувати це сама задача. Тобто, не всі завдання в кінцевому підсумку будуть заплановані паралельно один одному. Мати більше завдань, ніж можна виконати паралельно, це добреріч у цьому випадку; це означає, що, ймовірно, збільшується масштаб, оскільки ви додаєте більше ядер. Одним із недоліків цього є те, що для розробки пристойної черги та робочого циклу потрібна велика робота, якщо ви не маєте доступу до бібліотеки чи мови виконання, які вже надають це для вас. Найважче - переконатися, що ваші завдання справді відокремлені та захищені від ниток, і переконайтеся, що ваші завдання знаходяться в щасливій середині між грубозернистим і дрібнозернистим.

Інша альтернатива потокам підсистеми - це паралелізація кожної підсистеми ізольовано. Тобто, замість того, щоб запускати візуалізацію та фізику у власних потоках, запишіть підсистему «фізика», щоб використати всі ваші ядра відразу, напишіть підсистему візуалізації, щоб використати всі ваші ядра відразу, а потім дві системи просто запускаються послідовно (або перемежовуються, залежно від інших аспектів вашої архітектури гри). Наприклад, у підсистемі фізики ви можете взяти всі точкові маси в грі, розділити їх між вашими ядрами, а потім одразу оновити всі ядра. Після цього кожне ядро ​​може працювати над вашими даними у вузьких циклах із хорошою локальністю. Цей паралелізм стилю блокування схожий з тим, що робить GPU. Найважче тут - переконатися, що ви ділите свою роботу на дрібнозернисті шматки, такі, що розподіляють її рівномірнофактично призводить до рівного обсягу роботи у всіх процесорах.

Однак іноді просто простіше, через політику, існуючий код чи інші розчарувальні обставини, дати кожній підсистемі нитку. У такому випадку краще уникати створення більшої кількості потоків ОС, ніж ядер для великого навантаження процесора (якщо у вас є час виконання з легкими потоками, які просто зрівноважуються між вашими ядрами, це не так вже й багато). Також уникайте надмірного спілкування. Одна приємна хитрість - спробувати трубопровід; кожна основна підсистема може одночасно працювати над іншим ігровим станом. Трубопровідна робота знижує кількість необхідної комунікації між вашими підсистемами, оскільки не всі вони потребують доступу до одних і тих же даних одночасно, а також можуть звести нанівець частину шкоди, заподіяної вузькими місцями. Наприклад, якщо ваша фізична підсистема має тенденцію тривати довгий час, і ваша підсистема візуалізації закінчується завжди її чекаючи, ваша абсолютна частота кадрів може бути вищою, якщо ви запустите фізичну підсистему для наступного кадру, поки підсистема візуалізації все ще працює на попередній каркас. Насправді, якщо у вас є такі вузькі місця і ви не можете їх усунути будь-яким іншим способом, конвеєрна трубопровід може бути найбільш законною причиною турбуватися з потоками підсистеми.


"як тільки нитка потребує результату завдання, якщо завдання виконано, воно може отримати результат, а якщо ні, то можна безпечно видалити завдання з черги та йти вперед і виконувати це завдання самостійно". Ви говорите про завдання, породжене тією ж ниткою? Якщо так, то чи не було б більше сенсу, якщо це завдання виконується ниткою, яка породила саму задачу?
jmp97

тобто потік може, не плануючи завдання, виконати це завдання відразу.
jmp97

3
Справа в тому, що нитка не обов'язково знає спереду, чи було б краще запустити завдання паралельно чи ні. Ідея полягає в тому, щоб спекулятивно розпалити роботу, яку вам, зрештою, потрібно буде виконати, і якщо інша нитка виявиться непрацюючою, то вона може продовжувати і робити цю роботу за вас. Якщо це не відбудеться до того часу, коли вам потрібен результат, ви можете просто витягнути завдання з черги самостійно. Ця схема призначена для динамічного врівноваження навантаження через декілька ядер, а не статично.
Джейк МакАртюр

Вибачте за те, що зайняли стільки часу, щоб повернутися до цієї теми. Останнім часом я не звертаю уваги на гамедев. Це, мабуть, найкраща відповідь, тупа, але до суті і обширна.
j riv

1
Ви маєте рацію в тому сенсі, що я знехтував говорити про великі навантаження вводу / виводу. Моя інтерпретація запитання полягала в тому, що мова йшла лише про навантаження на процесор.
Джейк МакАртюр

30

Є кілька речей, які слід розглянути. Маршрут "по потоку на підсистему" легко думати, оскільки розділення коду досить очевидно з початку руху. Однак, залежно від того, скільки взаємозв'язку потребують ваші підсистеми, міжпотокове спілкування може дійсно вбити вашу ефективність. Крім того, це лише масштабування до N ядер, де N - кількість підсистем, які ви абстрагуєте в потоки.

Якщо ви просто шукаєте багатопотокові існуючі ігри, мабуть, це шлях найменшого опору. Однак, якщо ви працюєте над деякими системами двигунів низького рівня, які можуть бути розподілені між кількома іграми чи проектами, я б розглядав інший підхід.

Це може зайняти трохи скручування розуму, але якщо ви можете розбити речі як чергу черги на роботу з набором робочих ниток, вона в майбутньому буде значно кращою. Оскільки найновіші та найбільші фішки виходять із газильйонними ядрами, продуктивність вашої гри буде масштабуватися разом із нею, лише розпалюйте більше робочих ниток.

Отже, якщо ви хочете підкреслити деякий паралелізм існуючого проекту, я б паралелізував всі підсистеми. Якщо ви будуєте новий двигун з нуля, маючи на увазі паралельну масштабованість, я б заглянув у чергу роботи.


Система, яку ви згадуєте, дуже схожа на систему планування, згадану у відповіді, наданій Іншим Джеймсом, але все ще є хорошою деталізацією в цій галузі, тому +1, як це додає дискусії.
Джеймс

3
Вікі спільноти про те, як налаштувати чергу роботи та робочі теми, було б добре.
bot_bot

23

На це питання немає найкращої відповіді, оскільки це залежить від того, що ви намагаєтеся виконати.

Xbox має три ядра і може обробляти кілька потоків, перш ніж переключення контексту накладні стане проблемою. ПК може мати справу ще з кількома.

Для зручності програмування багато ігор, як правило, мають одну нитку. Це добре для більшості особистих ігор. Єдине, для чого, ймовірно, доведеться мати інший потік - це Мережа та аудіо.

У Unreal є ігровий потік, візуалізація, мережевий потік та аудіопотік (якщо я правильно пам'ятаю). Це досить стандартно для багатьох двигунів поточного покоління, хоча можливість підтримувати окремий потік візуалізації може бути болючим результатом і потребує великої основи.

Двигун idTech5, що розробляється для Rage, насправді використовує будь-яку кількість потоків, і це робить, розбиваючи ігрові завдання на "завдання", які обробляються системою завдань. Їх явна мета полягає в тому, щоб їхній ігровий механізм міг добре оцінюватись, коли кількість ядер у середній ігровій системі стрибає.

Технологія, яку я використовую (і написав), має окремий потік для мереж, вводу, аудіо, візуалізації та планування. Потім у ньому є будь-яка кількість потоків, які можна використовувати для виконання ігрових завдань, і цим керує потік планування. Дуже багато роботи було спрямовано на те, щоб усі потоки грали добре один з одним, але, здається, це добре працює і отримує дуже гарне використання багатоядерних систем, тому, можливо, це і є місією (поки що, я можу зламати аудіо / мережу / введіть роботу в просто "завдання", які робочі потоки можуть оновити).

Це дійсно залежить від вашої кінцевої мети.


+1 для згадки про систему планування. Зазвичай це гарне місце для центрування потоку / системної комунікації :)
Джеймс

Чому голос "за", "проти"?
jcora

12

Нитка на підсистему - це неправильний шлях. Раптом ваш додаток не буде масштабуватися, оскільки деякі підсистеми вимагають набагато більше, ніж інші. Це був підхід Верховного Командира, який не перевищував двох ядер, оскільки у них були лише дві підсистеми, які займали значну кількість процесорної та фізичної / ігрової логіки, хоча вони мали 16 потоків, інші потоки ледве склав будь-яку роботу, і в результаті гра розширилася лише до двох ядер.

Що ви повинні зробити, це використовувати щось, що називається пул потоків. Це дещо відображає підхід, застосований на графічних процесорах - тобто ви розміщуєте роботу, і будь-який доступний потік просто збирається і виконує це, а потім повертається до очікування роботи - подумайте про це як буфер кільця, з ниток. Цей підхід має перевагу N-ядерного масштабування і дуже добре підходить для масштабування як для низьких, так і для високих показників ядер. Недоліком є ​​те, що для цього підходу досить важко опрацювати право власності на потоки, оскільки неможливо знати, який потік виконує якусь роботу в будь-який момент часу, тому вам доведеться дуже чітко зафіксувати проблеми власності. Також дуже важко використовувати такі технології, як Direct3D9, які не підтримують декілька потоків.

Басейни з нитками дуже важкі у використанні, але вони дають найкращі можливі результати. Якщо вам потрібне надзвичайно гарне масштабування або у вас є достатньо часу для роботи над ним, використовуйте пул ниток. Якщо ви намагаєтесь внести паралелізм у існуючий проект із невідомими проблемами залежності та однопотоковими технологіями, це не для вас рішення.


Щоб бути трохи більш точним: графічні процесори не використовують пули потоків, натомість планувальник потоків реалізований апаратно, що робить дуже дешевим створення нових потоків та комутаційних потоків, на відміну від процесорів, де створення потоків та перемикачі контексту дорого коштують. Наприклад, див. Посібник для програмістів Nvidias CUDA.
Нілс

2
+1: найкраща відповідь тут. Я б навіть використовував більш абстрактні конструкції, ніж потокові пули (наприклад, черги на роботу та робітники), якщо ваша структура дозволяє. Набагато простіше думати / програмувати в цьому плані, ніж у чистих нитках / замках / тощо. Плюс: Розбиття вашої гри на візуалізації, логіці тощо - це нісенітниця, оскільки рендерінг повинен чекати, коли логіка закінчиться. Скоріше створіть завдання, які насправді можна виконати паралельно (наприклад: Обчисліть AI для одного npc для наступного кадру).
Дейв О.

@DaveO. Ваша точка "Плюс" така, така правдива.
Інженер

11

Ви маєте рацію, що найважливіша частина - це уникати синхронізації, де це можливо. Є кілька способів цього досягти.

  1. Знайте свої дані та зберігайте їх у пам'яті відповідно до ваших потреб обробки. Це дає змогу планувати паралельні обчислення без необхідності синхронізації. На жаль, цього в більшості випадків досить важко досягти, оскільки до них часто звертаються з різних систем у непередбачувані часи.

  2. Визначте чіткі терміни доступу до даних. Ви можете розділити основний галочок на x фази. Якщо ви впевнені, що Thread X читає дані лише на певній фазі, ви також знаєте, що ці дані можуть бути змінені іншими потоками на іншій фазі.

  3. Двократно завантажуйте дані. Це найпростіший підхід, але він збільшує затримку, оскільки Thread X працює з даними з останнього кадру, тоді як Thread Y готує дані для наступного кадру.

Мій особистий досвід показує, що дрібнозернисті обчислення є найефективнішим способом, оскільки вони можуть масштабуватися набагато краще, ніж рішення на основі підсистеми. Якщо ви потоку ваших підсистем, час кадру буде прив’язаний до найдорожчої підсистеми. Це може призвести до всіх потоків, але до одного простою, поки дорога підсистема остаточно не завершить свою роботу. Якщо ви в змозі відокремити великі частини вашої гри на невеликі завдання, ці завдання можна скласти відповідно, щоб уникнути простоїв ядер. Але це щось важко здійснити, якщо у вас вже є велика база коду.

Щоб взяти до уваги деякі технічні обмеження, вам слід намагатися ніколи не передплачувати своє обладнання. Маючи на увазі підписку, я маю на увазі наявність більше програмних потоків, ніж апаратні потоки вашої платформи. Особливо для архітектур PPC (Xbox360, PS3) перемикач завдань дійсно дорогий. Звичайно, цілком нормально, якщо у вас є кілька надмірних підписок на потоки, які спрацьовують лише протягом невеликої кількості часу (наприклад, один раз кадр). Якщо ви орієнтуєтесь на ПК, слід пам’ятати, що кількість ядер (або краще HW -Threads) постійно зростає, тому ви хочете знайти масштабоване рішення, яке скористається додатковою CPU-Power. Отже, в цій області вам слід спробувати розробити свій код якомога більше на основі завдань.


3

Загальне правило для введення програми в додаток: 1 нитка на Core CPU. На чотирьохядерному ПК це означає 4. Як було зазначено, XBox 360, однак, має 3 ядра, але 2 апаратних потоку в кожному, тому в цьому випадку 6 потоків. У такій системі, як PS3 ... удачі в цій :) :) Люди все ще намагаються це зрозуміти.

Я б запропонував розробити кожну систему як самодостатній модуль, який ви зможете ввести, якщо хочете. Зазвичай це означає наявність чітко визначених шляхів зв'язку між модулем та рештою двигуна. Мені особливо подобаються процеси лише для читання, такі як Візуалізація та аудіо, а також "чи є ми ще", такі як введення в програвач читання для того, щоб речі вийшли з потоку. Якщо доторкнутися до відповіді, яку дає AttackingHobo, коли ви робите 30-60 кадрів в секунду, якщо ваші дані на 1/30-ту / 60-ту секунду застаріли, це насправді не зашкодить чуйному почуттю вашої гри. Завжди пам’ятайте, що головна відмінність прикладного програмного забезпечення від відеоігор - це робити все 30-60 разів на секунду. Однак на цій же примітці

Якщо ви досить добре розробили системи вашого двигуна, будь-яку з них можна переміщувати з нитки на нитку, щоб завантажувати баланс вашого двигуна більш доцільно на основі гри тощо. Теоретично ви також можете використовувати свій двигун у розподіленій системі, якщо це потрібно, коли повністю окремі комп'ютерні системи працюють із кожним компонентом.


2
У Xbox360 є 2 апаратних нитки на ядро, тому оптимальна кількість потоків - 6.
DarthCoder

Ах, +1 :) Мене завжди обмежували сфери мереж 360 та PS3, хе-хе :)
Джеймс,

0

Я створюю один потік на логічне ядро ​​(мінус один, щоб враховувати головну нитку, яка, до речі, відповідає за візуалізацію, але в іншому випадку також виступає як робоча нитка).

Я збираю події пристрою введення в реальному часі протягом кадру, але не застосовую їх до кінця кадру: вони матимуть ефект у наступному кадрі. І я використовую подібну логіку для візуалізації (старий стан) проти оновлення (новий стан).

Я використовую атомні події для відкладання небезпечних операцій до пізніше в тому ж кадрі, і я використовую більше однієї черги подій (черги завдань) для того, щоб реалізувати бар'єр пам’яті, який дає гарантію, що відповідає залізо, щодо порядку операцій, без блокування та очікування (заблокуйте вільні паралельні черги у порядку пріоритетності роботи).

Примітно зазначити, що будь-яка робота може видавати підзадачі (які тонші та наближаються до атомності) до тієї ж черги пріоритету або тієї, що вище (подається пізніше у кадрі).

Враховуючи, що у мене є три такі черги, усі потоки, крім одного, можуть потенційно затримуватися рівно три рази на кадр (під час очікування, коли інші потоки виконають усі невиконані завдання, видані на поточному рівні пріоритету).

Це здається прийнятним рівнем бездіяльності потоку!


Мій кадр починається з ГОЛОВНОГО рендеринга СТАРОГО ДЕРЖАВИ з попереднього пропуску оновлення попереднього кадру, в той час як усі інші потоки негайно починають обчислювати NEXT стан кадру, я просто використовую події, щоб подвоїти зміни стану буфера до точки в кадрі, де ніхто більше не читає .
Гомер

0

Зазвичай я використовую один основний потік (очевидно), і я додаватиму тему щоразу, коли помічаю зниження продуктивності приблизно від 10 до 20 відсотків. Для пошуку такої краплі я використовую інструменти для виконання візуальної студії. Поширеними подіями є (не) завантаження деяких ділянок карти або проведення важких розрахунків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.