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

Должен ли я иметь отдельные потоки для рендеринга и логики или даже больше?

Я осознаю огромное падение производительности, вызванное синхронизацией данных (не говоря уже о любых блокировках мьютекса).

Я думал об этом предельно и делал потоки для мыслимой каждой мыслимой подсистемы. Но я беспокоюсь, что это может замедлить ситуацию. (Например, разумно ли разделять входной поток от потоков воспроизведения или игровой логики?). Требуется ли синхронизация данных сделать ее бессмысленной или даже более медленной?

81 голос | спросил j riv 12 Jpm1000000pmWed, 12 Jan 2011 23:14:07 +030011 2011, 23:14:07

8 ответов


60

Общий подход к использованию нескольких ядер, честно говоря, просто ошибочный. Разделение ваших подсистем на разные потоки действительно разделит некоторые работы на несколько ядер, но у него есть некоторые серьезные проблемы. Во-первых, с этим очень сложно работать. Кто хочет гасить с помощью замков, синхронизации и общения и прочее, когда они могут просто писать прямо на рендеринг или код физики? Во-вторых, подход фактически не расширяется. В лучшем случае это позволит вам воспользоваться возможно тремя или четырьмя ядрами, и вот если вы действительно знаете, что делаете. В игре всего так много подсистем, и из них еще меньше, чем занимают большие куски процессорного времени. Есть пара хороших альтернатив, которые я знаю.

Один из них - иметь основной поток вместе с рабочим потоком для каждого дополнительного ЦП. Независимо от подсистемы основной поток делегирует изолированные задачи рабочим потокам через какую-то очередь (-ы); эти задачи могут сами создавать и другие задачи. Единственная цель рабочих потоков - это каждый захват задач из очереди по очереди и их выполнение. Самое главное, однако, состоит в том, что, как только поток нуждается в результате задачи, если задача завершена, она может получить результат, а если нет, то можно безопасно удалить задачу из очереди и выполнить ее сама задача. То есть не все задачи в конечном итоге планируются параллельно друг с другом. В этом случае больше задач, чем может выполняться параллельно, - это good ; это означает, что он может масштабироваться по мере добавления большего количества ядер. Одним из недостатков этого является то, что для разработки достойного цикла очереди и рабочего цикла требуется много работы, если у вас нет доступа к библиотеке или языковой среде, которая уже предоставляет это для вас. Самая сложная часть - убедиться, что ваши задачи действительно изолированы и потокобезопасны, и убедитесь, что ваши задачи находятся в счастливой средней почве между крупнозернистыми и мелкозернистыми.

Другой альтернативой потокам подсистем является параллелизация каждой подсистемы изолированно. То есть вместо того, чтобы выполнять рендеринг и физику в своих потоках, напишите физическую подсистему, чтобы сразу использовать все ваши ядра, запишите подсистему рендеринга, чтобы сразу использовать все ваши ядра, а затем две системы просто запускаются последовательно (или чередуются, в зависимости от других аспектов вашей игровой архитектуры). Например, в подсистеме физики вы можете взять все точечные массы в игре, разделить их между вашими ядрами, а затем все ядра обновить их сразу. Затем каждое ядро ​​может работать с вашими данными в узких петлях с хорошей локальностью. Этот стиль блокировки параллелизма аналогичен тому, что делает GPU. Самое сложное здесь - убедиться, что вы делите свою работу на мелкозернистые куски, так что разделение ее равномерно на самом деле приводит к равному объему работы для всех процессоров.

Однако иногда это просто проще, из-за политики, существующего кода или других расстраивающих обстоятельств, чтобы дать каждой подсистеме поток. В этом случае лучше избегать создания большего количества потоков ОС, чем ядер для больших нагрузок процессора (если у вас есть среда выполнения с легкими потоками, которые просто балансируют между вашими ядрами, это не так важно для сделки). Также избегайте чрезмерного общения. Один хороший трюк - попробовать конвейерную обработку; каждая основная подсистема может работать одновременно с другим игровым состоянием. Конвейерная обработка уменьшает объем необходимой связи между вашими подсистемами, поскольку они не все нуждаются в доступе к одним и тем же данным одновременно, а также могут аннулировать часть ущерба, вызванного узкими местами. Например, если ваша физическая подсистема имеет тенденцию занимать много времени, и ваша подсистема рендеринга заканчивается, она всегда будет ее ждать, ваша абсолютная частота кадров может быть выше, если вы запустите подсистему физики для следующего кадра, пока подсистема рендеринга все еще работает на предыдущем кадре. Фактически, если у вас есть такие узкие места и они не могут удалить их каким-либо другим способом, конвейерная обработка может быть наиболее законной причиной для беспокойства в отношении потоков подсистем.

ответил Jake McArthur 7 FebruaryEurope/MoscowbMon, 07 Feb 2011 07:58:39 +0300000000amMon, 07 Feb 2011 07:58:39 +030011 2011, 07:58:39
30

Есть несколько вещей, которые нужно учитывать. Маршрут «нить-под-подсистему» ​​легко сообразить, поскольку разделение кода довольно очевидно из-за перехода. Однако, в зависимости от того, сколько межсоединений необходимы ваши подсистемы, межпоточная связь может действительно убить вашу производительность. Кроме того, это только масштабируется до N ядер, где N - количество подсистем, которые вы абстрактно в потоки.

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

Это может занять немного ума, но если вы можете разбить вещи как очередь заданий с набором рабочих потоков, в долгосрочной перспективе он будет значительно улучшаться. По мере того, как последние и самые большие чипы выходят с ядрами gazillion, производительность вашей игры будет масштабироваться вместе с ней, просто запустите больше рабочих потоков.

Итак, в принципе, если вы хотите приложить некоторый параллелизм к существующему проекту, я бы распараллеливал все подсистемы. Если вы создаете новый движок с нуля с параллельной масштабируемостью, я бы посмотрел в очередь заданий.

ответил Bob Somers 13 Jam1000000amThu, 13 Jan 2011 09:16:35 +030011 2011, 09:16:35
23

Этот вопрос не имеет лучшего ответа, поскольку он зависит от того, что вы пытаетесь выполнить.

xbox имеет три ядра и может обрабатывать несколько потоков, прежде чем переключение контекста станет проблемой. ПК может иметь дело с еще несколькими.

Многие игры, как правило, были однопоточными для простоты программирования. Это нормально для большинства личных игр. Единственное, что вы, вероятно, должны были бы для другого потока, это Networking and Audio.

Unreal имеет игровой поток, поток рендеринга, сетевой поток и аудиопоток (если я правильно помню). Это довольно стандартно для многих современных двигателей, хотя возможность поддержки отдельного потока рендеринга может быть больной и требует много оснований.

Двигатель idTech5, разработанный для Rage, фактически использует любое количество потоков, и он делает это, разбивая игровые задачи на «задания», которые обрабатываются с помощью системы задач. Их явная цель состоит в том, чтобы хорошо оценить их игровой движок, когда число ядер в средней игровой системе скачкообразно растет.

В технологии, которую я использую (и написал), есть отдельная тема для работы в сети, входе, аудио, рендеринге и планировании. Затем он имеет любое количество потоков, которые могут использоваться для выполнения игровых задач, и это управляется потоком планирования. Работа много заключалась в том, чтобы все нити играли красиво друг с другом, но, похоже, они хорошо работают и очень хорошо используют многоядерные системы, поэтому, возможно, это миссия (на данный момент; Я мог бы сломать работу аудио /сети /ввода в просто «задачи», которые могут обновлять рабочие потоки).

Это действительно зависит от вашей конечной цели.

ответил James 13 Jam1000000amThu, 13 Jan 2011 00:29:54 +030011 2011, 00:29:54
12

Поток для каждой подсистемы - неправильный путь. Внезапно ваше приложение не будет масштабироваться, потому что некоторые подсистемы требуют гораздо больше, чем другие. Это был подход, применяемый Верховным Главнокомандующим, и он не масштабировался за пределами двух ядер, потому что у них было только две подсистемы, которые занимали значительное количество процессоров и физико-игровой логики, хотя у них было 16 потоков, а другие потоки едва сводится к какой-либо работе и, как результат, игра только масштабируется до двух ядер.

Что вы должны сделать, это использовать что-то, называемое пулом потоков. Это несколько отражает подход, применяемый к графическим процессорам, то есть вы публикуете работу, и любой доступный поток просто приходит и делает это, а затем возвращается в ожидании работы - думайте об этом как кольцевой буфер, о потоках. Преимущество такого подхода заключается в масштабировании N-ядер и очень хорошем при масштабировании как для низкого, так и для большого числа ядер. Недостатком является то, что для этого подхода довольно сложно работать с правами потока, так как невозможно узнать, какая нить выполняет то, что работает в любой момент времени, поэтому вам необходимо очень сильно заблокировать права собственности. Также очень сложно использовать такие технологии, как Direct3D9, которые не поддерживают несколько потоков.

Пулы потоков очень трудны в использовании, но они обеспечивают наилучшие результаты. Если вам нужно очень хорошее масштабирование или у вас достаточно времени для работы с ним, используйте пул потоков. Если вы пытаетесь внедрить параллелизм в существующий проект с неизвестными проблемами зависимостей и однопоточными технологиями, это не решение для вас.

ответил DeadMG 15 Jam1000000amSat, 15 Jan 2011 01:34:28 +030011 2011, 01:34:28
11

Вы правы, что самая важная часть - избегать синхронизации, где это возможно. Есть несколько способов добиться этого.

  1. Знайте свои данные и храните их в памяти в соответствии с вашими потребностями в обработке. Это позволяет планировать параллельные вычисления без необходимости синхронизации. К сожалению, в большинстве случаев это довольно сложно добиться, поскольку данные часто получают доступ из разных систем в непредсказуемое время.

  2. Определите время доступа к данным. Вы можете разделить свой основной тик на x фаз. Если вы уверены, что Thread X считывает данные только в определенной фазе, вы также знаете, что эти данные могут быть изменены другими потоками в другой фазе.

  3. Дважды буферизуйте свои данные. Это самый простой подход, но он увеличивает задержку, поскольку Thread X работает с данными из последнего кадра, тогда как Thread Y готовит данные для следующего кадра.

Мой личный опыт показывает, что мелкозернистые вычисления являются наиболее эффективным способом, поскольку они могут масштабироваться намного лучше, чем решения на основе подсистем. Если вы введете свои подсистемы, время фрейма будет привязано к самой дорогостоящей подсистеме. Это может привести к тому, что все потоки будут оставаться на одном холостом ходу, пока дорогая подсистема не завершит свою работу. Если вы можете отделить большие части вашей игры от небольших задач, эти задачи можно планировать соответственно, чтобы избежать холостых ядер. Но этого трудно достичь, если у вас уже есть большая база кода.

Чтобы принять во внимание некоторые аппаратные ограничения, вы должны стараться никогда не переписывать свое оборудование. С переподпиской я имею в виду больше программных потоков, чем ваши аппаратные потоки платформы. Особенно на архитектуре PPC (Xbox360, PS3) задача-коммутатор действительно стоит дорого. Это, конечно, отлично, если у вас есть несколько потоков, которые были переписаны, которые запускаются только в течение небольшого промежутка времени (например, для кадра) Если вы нацеливаетесь на ПК, вы должны помнить, что количество ядер (или лучше HW-Threads) постоянно растет, поэтому вы хотите найти масштабируемое решение, которое использует преимущества дополнительного CPU-Power. Итак, в этой области вы должны попытаться сконструировать свой код как можно более точно.

ответил DarthCoder 13 Jpm1000000pmThu, 13 Jan 2011 12:56:08 +030011 2011, 12:56:08
3

Общее правило для потоковой передачи приложения: 1 поток на процессор Core. На четырехъядерном ПК, что означает 4. Как было отмечено, XBox 360, однако, имеет 3 ядра, но 2 аппаратных потока каждый, поэтому в этом случае 6 потоков. На такой системе, как PS3 ... хорошо удача в этом :) Люди все еще пытаются понять это.

Я бы предложил создать каждую систему как автономный модуль, который вы могли бы использовать, если хотите. Это обычно означает наличие четко определенных путей связи между модулем и остальной частью двигателя. Мне особенно нравятся процессы только для чтения, такие как рендеринг и аудио, а также «есть ли у нас там» процессы, такие как чтение проигрывателя для того, чтобы вещи были пропущены. Чтобы прикоснуться к ответу, заданному AttackingHobo, когда вы показываете 30-60 кадров в секунду, если ваши данные 1/30-й-1/60-й секунды устарели, это действительно не умаляет отзывчивое чувство вашей игры. Всегда помните, что основное различие между прикладным программным обеспечением и видеоиграми делает все 30-60 раз в секунду. Однако на этом же входе может быть одна из вещей, которые вы хотите сохранить в основном потоке, чтобы остальные могли реагировать на нее, как только она появляется:)

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

ответил James 13 Jam1000000amThu, 13 Jan 2011 04:22:42 +030011 2011, 04:22:42
0

Я создаю один поток для каждого логического ядра (минус один, для учета Main Thread, который, кстати, отвечает за рендеринг, но в противном случае действует как рабочий поток).

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

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

Следует отметить, что любое задание может выдавать подзаголовки (которые более тонкие и приближающиеся атомарности) к одной и той же очереди приоритетов или к той, которая выше (будет показана позже в кадре).

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

Это кажется приемлемым уровнем бездействия нити!

ответил Homer 27 +03002014-10-27T10:16:05+03:00312014bEurope/MoscowMon, 27 Oct 2014 10:16:05 +0300 2014, 10:16:05
0

Обычно я использую один основной поток (очевидно), и я добавлю поток каждый раз, когда я замечаю падение производительности примерно на 10-20%. Чтобы справиться с такой проблемой, я использую инструменты для визуальной студии. Общие события (un) загружают некоторые области карты или делают некоторые тяжелые вычисления.

ответил Lenard Arquin 7 AM000000120000002431 2015, 00:01:24

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132