Основы разработки многопоточных игровых приложений

Создать новую статью

13.08.2009 13:00


Воспользуйтесь преимуществами двухъядерных процессоров Intel® при разработке игр и узнайте, как оптимизировать код с помощью компиляторов Intel®.

Введение

Несмотря на очевидные преимущества технологии Hyper-Threading, многопоточность в игровых приложениях не реализована из-за своей потенциальной сложности. Сейчас, когда в распоряжении пользователей есть двухъядерные процессоры Intel® Pentium® D и Intel® Pentium® 4 Extreme Edition (который к тому же поддерживает технологию Hyper-Threading), количество логических процессоров может быть увеличено до четырех. Осталось воспользоваться этим и увеличить производительность игр как минимум вдвое. Конечно, для этого в них необходимо организовать поточную обработку.

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

Данная статья предназначена специально для архитекторов и разработчиков игровых приложений, которых автор пытается убедить в необходимости реализовывать многопоточность непосредственно в ходе разработки приложений. Таким образом, игры будут использовать все преимущества двух (или даже четырех) процессоров в системах на базе платформ Intel®. Придет время, когда количество ядер в процессорах увеличится еще более, а требования к многопоточности игровых приложений возрастут. Разумеется, это позволит достичь очевидного прироста производительности.

Что такое многопоточность?

Под многопоточностью понимается одновременная обработка различных разделов программного кода двумя и более процессорами. На рис. 1 иллюстрируется разница между однопоточной и двухпоточной обработкой данных. При однопоточной обработке происходит последовательное выполнение программного кода, а при многопоточной – одновременная обработка нескольких потоков. Очевидно, что при параллельной обработке кода на его выполнение требуется меньше времени.

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

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

Модели параллельных вычислений

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

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

Правильная организация потоков

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

Определите функциональные блоки

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

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

Определите зависимость между функциональными блоками

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

Разделите код на потоки

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

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

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

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

Многопоточность в игровых приложениях

Определив, какие блоки кода будут обрабатываться параллельно, вы вплотную подошли к созданию потоков. Это можно сделать несколькими способами. Например, можно воспользоваться прикладным программным интерфейсом операционной системы, для которой предназначено ваше приложение, или универсальным прикладным программным интерфейсом. Еще можно использовать библиотеку OpenMP*, компилятор которой имеет встроенную поддержку организации поточной обработки. Учитывая, что OpenMP содержит средства и для создания, и для синхронизации потоков, предпочтительнее и проще выбрать именно ее. С библиотекой OpenMP могут работать несколько компиляторов, например, компилятор Intel® C++ Compiler.

Разделы кода OpenMP

С помощью указателей разделов OpenMP легко организуются потоки различных функциональных блоков. На рис. 4 представлен раздел кода, в котором для параллельного выполнения четырех разных функций организовано два потока. Как видно из рисунка, для этого потребовалось всего лишь три строчки кода (не считая строк со скобками).

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

На рис. 4 отмечена строка кода, до которой приложение ждет, пока закончится обработка всех потоков.

Необходимо отметить, что даже использование OpenMP не гарантирует абсолютной надежности вашего кода, поэтому не стоит пренебрегать проверкой условий организации потоков.

Организация очереди задач с помощью компилятора Intel®

Компилятор Intel C++ compiler содержит дополнение к OpenMP, с помощью которого можно создавать очереди задач внутри пула потоков. Таким образом, для выполнения кода будут использованы все процессоры системы (если, конечно, в вашем коде количество параллельных функций не меньше количества процессоров).

На рис. 5 приведен пример организации очереди задач при разделении функций на потоки. Указатели на каждую задачу определяют порядок функций в очереди, согласно которой они выполняются процессором.

При такой организации код будет оптимизирован под любое количество процессоров. Однако, с учетом того, что количество процессоров в системе неизвестно, вам необходимо будет организовать оптимальное формирование очереди функций. С помощью описанного метода, для выполнения кода, разделенного на потоки с использованием OpemMP, будут задействованы все процессоры системы.

Выводы

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

Дополнительные ресурсы

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

Инструменты для параллельного программирования, представленные ниже, можно также найти на сайте сообщества Intel® Software Network и страницах программной продукции.

Об авторе

Джефф Эндрюс – проектировщик приложений в корпорации Intel, специализируется на оптимизации кода для независимых поставщиков ПО. Он также занимается исследованиями в области программных технологий, повышающих производительность приложений при работе с процессорами Intel.