- PVSM.RU - https://www.pvsm.ru -
Часть 1: Введение [1]
Часть 2: Многопоточность
Часть 3: Рендеринг (Прим. пер. — в процессе перевода)
Часть 4: Doom classic integration (Прим. пер. — в процессе перевода)
Движок для Doom III был написан в период с 2000 по 2004 год, в то время, когда большинство ПК были однопроцессорными. Хотя архитектура движка idTech4 разрабатывалась с учетом поддержки SMP, это закончилось тем, что поддержка многопоточности делалась в последнюю минуту (см. интревью с Джоном Кармаком [2]).
С тех пор изменилось многое, есть хорошая статья от Microsoft "Программирование для многоядерных систем [3]":
В течение многих лет производительность процессоров неуклонно возрастала, и игры, и другие программы получали выгоду от этого увеличения мощности без необходимости прикладывать усилия.
Правила изменились. Производительность одноядерных процессоров в настоящее время растет очень медленно, если вообще растет. Однако, вычислительные мощности персональных компьютеров и консолей продолжает расти. Разница лишь том, что в основном такой прирост теперь получается за счет наличия многоядерных процессоров.
Прирост мощности процессора так же впечатляющ, как и раньше, но теперь разработчики должны писать многопоточный код для того, чтобы полностью раскрыть потенциал этой мощности.
Целевые платформыи Doom III BFG многоядерны:
В результате idTech4 был усилены не только поддержкой многопоточности, но и компонентом idTech5 «Job Processing System», добавляющий поддержку многоядерных систем.
К сведению: не так давно были обнародованы спецификации Xbox Ona и PS4: оба будут иметь по восемь ядер. Еще одна причина, для любого разработчика игр хорошо разбираться в многопоточном программировании.
На PC игра запускается в трех потоках:
Кроме того, idTech4 создает еще два рабочих потока. Они необходимы для помощи любому из трех основных потоков. Они управляются планировщиком, когда это возможно.
Id Software обнародовало решение проблем многоядерного программирования в 2009 в презентации "Beyond Programming Shaders [6]". Две основные идеи тут:
Система состоит из 3х компонентов:
Задачи это именно то, что можно было бы ожидать:
struct job_t {
void (* function )(void *); // Job instructions
void * data; // Job parameters
int executed; // Job end marker...Not used.
};
Примечание: В соответствии с комментариями в коде, «задание должно длиться по крайней мере пару 1000 тактов для того, чтобы перевесить издержки переключения. С другой стороны задание не должно длиться не более чем несколько 100 000 тактов для поддержания хорошего баланса нагрузку между несколькими процессами.
Обработчик представляет собой поток, который будет оставаться неактивным в ожидании сигнала. Когда он активирован он пытается найти задание. Обработчики стараются избегать синхронизации, используя атомарные операции, пытаясь получить задание из общего списка.
Синхронизация осуществляется через три примитива: сигналы, мьютексы и атомарные операции. Последний являются предпочтительным, так как позволяет двигателю сохранять фокусировку CPU. Эти три механизма реализации подробно описаны в нижней части этой страницы [7].
И первая идея обхода синхронизации: разделить списки заданий на несколько секций, к каждому из которых обращается только один поток и, следовательно, синхронизация не требуются. В движке такие очереди называются idParallelJobList.
В Doom III BFG присутствуют только три секции:
На PC при запуске создаются два рабочих потока, но, вероятно, в XBox360 и PS3 их создается больше.
По данным 2009 презентацию, в idTech5 добавлено больше секций:
Примечание: В презентации также упоминает концепция задержки в один кадр, но эта часть кода не относится к Doom III BFG.
Запущенные обработчики постоянно находятся в ожидании задания. Этот процесс не требует использования мьютексов или мониторов: атомарная инкрементация распределяет задания без перекрытия.
Поскольку задания разделены на секции доступные только в одному потоку, в синхронизации нет необходимости. Однако, предоставлениезадач обработчику системы действительно подразумевают мьютекс:
//tr.frontEndJobList is a idParallelJobList object.
for ( viewLight_t * vLight = tr.viewDef->viewLights; vLight != NULL; vLight = vLight->next ) {
tr.frontEndJobList->AddJob( (jobRun_t)R_AddSingleLight, vLight );
}
tr.frontEndJobList->Submit();
tr.frontEndJobList->Wait();
Обработчики выполняются в бесконечном цикле. В каждой итерации проверяется кольцевой буфер, и если задание найдено — оно копируется ссылкой в локальный стек.
Локальный стек: стек потока используется для хранения адресов JobLists для предотвращение остановки механизма. Если поток не может «заблокировать» JobList, она падает в RUN_STALLED режим. Это остановка может быть отменена путем перехода стека из локального JobLists в общий список.
Интересно то, что все будет сделано без каких-либо взаимных механизмов: только атомарные операции.
int idJobThread::Run() {
threadJobListState_t threadJobListState[MAX_JOBLISTS];
while ( !IsTerminating() ) {
int currentJobList = 0;
// fetch any new job lists and add them to the local list in threadJobListState
{}
if ( lastStalledJobList < 0 )
// find the job list with the highest priority
else
// try to hide the stall with a job from a list that has equal or higher priority
currentJobList = X;
// try running one or more jobs from the current job list
int result = threadJobListState[currentJobList].jobList->RunJobs( threadNum, threadJobListState[currentJobList], singleJob );
// Analyze how job running went
if ( ( result & idParallelJobList_Threads::RUN_DONE ) != 0 ) {
// done with this job list so remove it from the local list (threadJobListState[currentJobList])
} else if ( ( result & idParallelJobList_Threads::RUN_STALLED ) != 0 ) {
lastStalledJobList = currentJobList;
} else {
lastStalledJobList = -1;
}
}
}
int idParallelJobList::RunJobs( unsigned int threadNum, threadJobListState_t & state, bool singleJob ) {
// try to lock to fetch a new job
if ( fetchLock.Increment() == 1 ) {
// grab a new job
state.nextJobIndex = currentJob.Increment() - 1;
// release the fetch lock
fetchLock.Decrement();
} else {
// release the fetch lock
fetchLock.Decrement();
// another thread is fetching right now so consider stalled
return ( result | RUN_STALLED );
}
// Run job
jobList[state.nextJobIndex].function( jobList[state.nextJobIndex].data );
// if at the end of the job list we're done
if ( state.nextJobIndex >= jobList.Num() ) {
return ( result | RUN_DONE );
}
return ( result | RUN_PROGRESS );
}
Id Software использует три типа механизмов синхронизации:
1. Мониторы (idSysSignal):
Абстракция | Операция | Реализация | Примечание |
idSysSignal | Event Objects [9] | ||
Raise | SetEvent [10] | Устанавливает указанное событие объекта в сигнальное состояние. | |
Clear | ResetEvent [11] | Устанавливает указанное событие объекта в несигнальное состояние. | |
Wait | WaitForSingleObject [12] | Ожидает, пока указанный объект будет находиться в сигнальном состоянии или пока время ожидания не истекло. |
Сигналы используется для остановки потока. Обработчики использует idSysSignal.Wait (), чтобы удалить себя из планировщика операционной системы, если задания отсутствуют.
2. Мьютексы (idSysMutex) :
Абстракция | Операция | Реализация | Примечание |
idSysMutex | Critical Section Objects [9] | ||
Lock | EnterCriticalSection [13] | Ожидает получения указанного объекта критической секции. Функция возвращается, когда вызывающий поток получил в собственность. | |
Unlock | LeaveCriticalSection [14] | Реализует получение указанного объекта критической секции. | |
3. Атомарные операции (idSysInterlockedInteger) :
Абстракция | Операция | Реализация | Примечание |
idSysInterlockedInteger | Interlocked Variables [15] | ||
Increment | InterlockedIncrementAcquire [16] | Инкрементация значение заданной 32-битовой переменной в качестве атомарной операции.Операция выполняется при помощи выделения семантической памяти (acquire memory ordering semantics). | |
Decrement | InterlockedDecrementRelease [17] | Декрементация значение заданной 32-битовой переменной в качестве атомарной операции.Операция выполняется при помощи выделения семантической памяти (acquire memory ordering semantics). |
Автор: PopeyetheSailor
Источник [18]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/35721
Ссылки в тексте:
[1] Часть 1: Введение: http://habrahabr.ru/post/180973/
[2] см. интревью с Джоном Кармаком: http://fabiensanglard.net/doom3/interviews.php
[3] Программирование для многоядерных систем: http://msdn.microsoft.com/en-ca/library/windows/desktop/ee416321(v=vs.85).aspx
[4] Xbox 360: http://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D1%85%D0%B0%D1%80%D0%B0%D0%BA%D1%82%D0%B5%D1%80%D0%B8%D1%81%D1%82%D0%B8%D0%BA%D0%B8_Xbox_360
[5] PS3 : http://ru.wikipedia.org/wiki/%D0%A2%D0%B5%D1%85%D0%BD%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D0%B5_%D1%85%D0%B0%D1%80%D0%B0%D0%BA%D1%82%D0%B5%D1%80%D0%B8%D1%81%D1%82%D0%B8%D0%BA%D0%B8_PlayStation_3
[6] Beyond Programming Shaders: http://s09.idav.ucdavis.edu/talks/05-JP_id_Tech_5_Challenges.pdf
[7] описаны в нижней части этой страницы: #syn_tools
[8] Мозгом: http://www.braintools.ru
[9] Event Objects: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686915(v=vs.85).aspx
[10] SetEvent: http://msdn.microsoft.com/en-us/library/windows/desktop/ms686211(v=vs.85).aspx
[11] ResetEvent: http://msdn.microsoft.com/en-us/library/windows/desktop/ms685081(v=vs.85).aspx
[12] WaitForSingleObject: http://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx
[13] EnterCriticalSection: http://msdn.microsoft.com/en-us/library/windows/desktop/ms682608(v=vs.85).aspx
[14] LeaveCriticalSection: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684169(v=vs.85).aspx
[15] Interlocked Variables: http://msdn.microsoft.com/en-us/library/windows/desktop/ms684122(v=vs.85).aspx
[16] InterlockedIncrementAcquire: http://msdn.microsoft.com/en-ca/library/windows/desktop/ms683618(v=vs.85).aspx
[17] InterlockedDecrementRelease: http://msdn.microsoft.com/en-us/library/windows/desktop/ms683586(v=vs.85).aspx
[18] Источник: http://habrahabr.ru/post/181081/
Нажмите здесь для печати.