Библиотека от AMD стала причиной плохой производительности процессоров AMD в Cyberpunk 2077
После выхода Cyberpunk 2077 пользователи процессоров AMD заметили, что игра не полностью использует все логические ядра. Выглядело это следующим образом:
Первоначально «конспирологические теории» базировались на том, что разработчики якобы использовали компилятор Intel C++, способный иногда компилировать код, который работает медленнее на процессорах конкурента (AMD) из-за неполного использования SIMD. Однако игра собрана компилятором Visual Studio. Впоследствии в дизассемблированном коде самой игры был найден фрагмент, проверяющий, запущена ли игра на процессоре AMD или же на другом процессоре — в случае, если игра запускается на процессоре AMD и, если процессор оказывается старше поколения Bulldozer, устанавливающий число потоков игрового планировщика равным числу физических ядер.
Этот код принадлежит библиотеке AMD GPUOpen cpu-core-counts, можете взглянуть на него:
// This advice is specific to AMD processors and is
// not general guidance for all processor
// manufacturers. Remember to profile!
DWORD getDefaultThreadCount() {
DWORD cores, logical;
getProcessorCount(cores, logical);
DWORD count = logical;
char vendor[13];
getCpuidVendor(vendor);
if (0 == strcmp(vendor, "AuthenticAMD")) {
if (0x15 == getCpuidFamily()) {
// AMD "Bulldozer" family microarchitecture
count = logical;
}
else {
count = cores;
}
}
return count;
}
Как мы видим, код написан самой AMD, для него нет документации, но есть небольшая статья, дающая разработчикам игр рекомендации о том, сколько потоков желательно использовать в планировщике игры. Главный посыл статьи заключается в том, что разработчики игр должны сами проводить тесты производительности и подбирать лучшую конфигурацию потоков планировщика (выделение мое):
Для современных процессоров Ryzen с включенным SMT подавляющее большинство многопоточных игр и приложений работают лучше, если число потоков равно числу логических ядер. Но у нас есть негативный опыт с небольшим числом игр, когда увеличение числа потоков сверх числа физических ядер, снижает производительность из-за исчерпания доступных исполнительных блоков.Но для процессоров поколения Bulldozer мы всегда рекомендуем устанавливать число потоков равным числу логических ядер. Остальные производители (Intel) могут давать свои рекомендации.
В любом случае, независимо от модели и производителя процессора, мы настоятельно рекомендуем разработчикам провести собственные тесты и принять решение на их основе. Наш пример кода [приведен мной выше], ошибочно осторожничает для процессоров Ryzen и тем самым призывает провести тесты производительности: функция
getDefaultThreadCount()
привлекает внимание тем, что возвращает значение, равное числу физических ядер.
For today«s Ryzen processors with SMT enabled, we«ve found that the vast majority of multithreaded games and applications work and scale really well when managing an active thread pool up to the number of logical cores that the processor supports. However, our experience with a small number of games is that driving a hardware thread pool with more than the number of physical cores can reduce performance, primarily due to contention for available per-core resources by the multiple running hardware threads.
However, for our own prior generation of Bulldozer-based processors designs, we recommend a default thread count equal to the number of logical processor cores. Other processor vendors are encouraged to provide their own guidance to software developers. AMD does not provide guidance for other processor vendors.
Therefore no matter the processor or processor vendor, we strongly recommend that you profile your games extensively to make a decision on how to manage your thread pool for the processor designs you«ll find your game code running on. Our sample code, linked below, errs on the side of caution for our Ryzen processors and encourages you to profile: the getDefaultThreadCount () function draws attention to that fact, returning a starting default count equal to the number of physical processor cores on Ryzen.
Кажется очевидным, что число потоков должно быть равным числу логических ядер процессора. Все дело в том, что код, активно использующий векторные инструкции в любом случае может полностью загрузить все исполнительные блоки ядра, максимально загрузить шину памяти и кэши. Отсюда следует, что использование SMT будет приводить только к ненужному сбросу кэша инструкций, TLB, блока предвыборки кода и предсказателя ветвлений. С другой стороны, «неоднородный код», где работа с памятью происходит в случайных местах будет работать быстрее со включенным SMT, который снизит задержки доступа к памяти.
AMD пыталась перестраховаться и подтолкнуть разработчиков игр к созданию собственных замеров производительности. Однако для этого компания выбрала максимально странный подход, а именно специально сделала в своем коде, не снабженным внятной документацией, неочевидную ошибку. К сожалению, в реальности даже ААА разработчики игр (CDPR) просто взяли библиотеку в готовом виде и применили ее, не разобравшись в тонкостях ее работы.
Логичнее было бы библиотеке всегда рекомендовать число потоков, равное числу логических ядер, и иметь соответствующую документацию, где бы доступно объяснялось, почему в некоторых случаях игнорирование SMT может дать больше производительности. Сама AMD утверждает, что SMT дает прирост производительности в подавляющем большинстве случаев. Библиотеке, на мой взгляд, не помешало бы предоставлять возможность обхода процедуры автоопределения и получать значение числа потоков для планировщика из переменной окружения, чтобы упростить тестирование для разработчиков и позволить пользователям решить возможные проблемы без необходимости перекомпиляции.
После отключения функции getDefaultThreadCount()
использование процессора увеличилось в случае Ryzen 5 1600 с 60% до 85%, Ryzen 5 1400 с 60% до 90%.
Получается, что сама AMD из-за ошибки в коде занижает производительность своих процессоров в играх относительно Intel. Неизвестно сколько еще игр «неправильно» используют библиотеку и в связи с этим хуже работают на процессорах AMD. Наверное, если бы функция getDefaultThreadCount()
всегда возвращала 1
, это могло бы заставить разработчика усомнится в корректности ее работы и почитать рекомендации AMD, но комментарий Remember to profile!
скорее всего ускользнёт от внимания инженера, занятого более важными задачами, чем точный подбор числа потоков планировщика для каждой модели процессора.
Мне удалось получить комментарий разработчика игр о том, почему такое случается:
Проекты настолько большие, что зачастую многое теряется и перестает контролироваться разработчиками. Разработчики совершают такие ошибки потому что в компании для тестирования игры может быть всего две-три типовых конфигурации компьютера и они, как мы понимаем, не покрывают все разнообразие возможных конфигураций. К тому же имеет место быть так называемый синдром «я просто выполняю свою работу (над маленьким куском, а на остальное мне пофиг)». Теоретически все «косяки» можно исправить грамотными схемами управления, но в условиях постоянно меняющихся требований и рынка мы имеем то, что имеем.