Book logo

Нашел интересную статью. Что бы я делал без copy-n-paste 😉

Продолжу жаловаться на жизнь графического программиста.

DIP – это DrawIndexedPrimitive, основная команда для отрисовки на видеокарте. Бывают и другие, но так как эта самая частая – принято их все называть DIP. Собственно, все программирование 3D-графики – это настройки видеокарты + вызов этой самой команды. И чуть пришлось прервать рендеринг из-за этих самых настроек (текстурку там сменить, матрицу траснформации или еще что), надо делать новый DIP.

Делать много DIP – плохо и медленно, и на сейчас это один из основных факторов, влияющих на производительность. В основном DIP сжирают CPU-время, хотя и GPU тоже. В DX стоимость сильно выше, считается из-за того, что для вызова драйвера нужен переход в ring0, который шибко медленный, а в OGL переходить в ring0 не надо. На самом деле, мне кажется, это не единственный фактор. Т.е. мне кажется, что драйвер DX реально толще, потому что больше игр на DX и больше “тюнинга” в драйвере (про “тюнинг” в драйвере вы, наверное, уже часто читали).

В мего-бумаге Batch, Batch, Batch измеряется, сколько батчей занимают целиком гигагерцовый CPU. ATI рекомендует делать не больше 500 DIPs/frame, и в этот лимит успешно никто не вписывается. Всегда считайте, сколько процессора жрет ваш рендер. Затраты CPU на общение с DirectX и драйвером легко может достигать половины кадра. На самом деле все еще сложнее, но об этом позже.

О боттлнеках

Перформанс в играх живет боттлнеками. “Код без боттлнеков – плохой код” (с) [info]boris_batkin Если у вас нет боттлнеков – значит, у вас толстый уровень управления и вы тратите процессорное время на ветер. Это не всегда плохо, кстати. Умение писать плохой код в нужных местах – исключительно важный скилл. Да-да-да, даже в коде игры, не говоря уже о скриптах. К счастью, в играх часто бывает нужно писать и хороший.
Боттлнеки бывают разного уровня. Бывают близкие к глобальным – первый вечный вопрос “CPU или GPU limited” при оптимизации. К сожалению, ответ даже на этот вопрос бывает очень сложным (неперпеливым – см. уже почти классические камменты к статье на gamedev.ru).
Бывают боттлнеки поменьше – вот этот эффект уперт в vertex shader, вот этот расчет на CPU в траффик памяти, вот эти патиклы fillrate limited и так далее. Классические примеры плохих “не-боттлнеков” – аллокации памяти, смены шейдеров, случайный доступ к памяти.

Лучший способ померять GPU limited вы или нет – изменить разрешение текстур, экрана, поиграться настройками antialiasing, изменить качество эффектов и длину шейдеров, и посмотреть изменился ли FPS. Если изменился – значит, хотя бы частично уперты. “Пацанские методы” типа измерения времени мест синхронизации могут обманывать.

Теперь непосредственно о том, почему все плохо.

О том, что внутри D3D

В DX SDK есть замечательная статья “Accurately Profiling Direct3D API Calls” (даже не нужно уточнять, что это must read для графикс-программеров), из которой мы узнаем, что непосредственно измерение времени DIP’a на CPU – ничего не дает. Чтобы уменьшить цену перехода ring0, DirectX накапливает все команды API во внутренний command buffer. Отмечу, что это аппаратно-независимый буфер, в котором токены почти целиком соответствуют командам DX (типа там, SetStreamSource, SetTexture и т.д.), и лучшие из нас даже видели его в дизассемблере. Когда этот буфер переполняется, то происходит переход в ring0 и содежимое буфера сливается в драйвер. Это может произойти при любом вызове DX, не обязательно DIP, и соответственно переход в драйвер происходит не в каждом DIP. Т.е. установил малюсенький стейт – и превед, есть шансы на десятки тысяч тактов.
Соответственно, измерение времени одного и даже группы вызовов DX – вещь крайне субъективная. Когда там еще тот флаш в драйвер произойдет. Есть чит – можно сделать GetData(D3DGETDATA_FLUSH) эвенту, и тогда текущий command buffer насильно сольется в драйвер. Так уже можно мерять CPU, но ценой общей производительности (происходит больше вызовов в ring0).

К дальнейшему – просьба на всякий случай относиться, как к моему воображению. Как там TomF пишет….
This email is the product of your deranged imagination, and does not in any way imply existence of the author.

Посмотрим, что происходит в драйвере, когда в него слили command buffer. У драйвера есть уже свой собственный буфер команд, которым он общается с видеокартой (это уже зависимые от видеокарты команды, которых не знает никто, кроме вендоров видеокарт и изредка главного иппонца). Производители видеокарт даже не обижаются, если при них его называют “push buffer”. Так вот, каждый раз драйвер парсит command buffer, медитирует над этими командами и заливает уже в push buffer команды для видеокарты. Она, понятно, работает параллельно и понемножку их выполняет.

Интересно начинается, если мы GPU limited. В этом случае мы отдаем команды видеокарте по определению быстрее, чем она может их выполнять. По правилам, драйвер имеет право синхронизироваться на Present раз в четыре кадра, и он обычно им пользуется. Это редко, и за эти четыре кадра push buffer может переполниться. Т.е. драйвер уже превысил размер пула, и не может дописать дальше. В этом случае драйверу ничего не остается, кроме как ждать пока не освободится место, т.е. пока карточка не выполнит свою работу.

И хоп – мы получили частичную синхронизацию CPU и GPU на ровном месте. Началось с того, что изменили маленький стейт, а оказалось даже видеокарту успели подождать. Т.е. померять нагрузку от CPU в такой ситуации вообще не получится, потому что измеряем и некоторую часть GPU. Хуже того, бывают случаи, когда такой “столл” становится вообще основной формой синхронизации, и традиционному Present остается чуть-чуть. CPU и GPU становятся страшно перемешаны, и вопрос “кто сколько жрет” становится еще тяжелее.

Любой вызов D3D – потенциальная точка GPU-синхронизации.

И чего?

Это все плохо измеримо и контролируемо, вот чего. Балансировка становится тяжелой работой – возможны места, где ограничивает GPU, и где CPU в пределах одного кадра. Это крайне плохо – уходит параллельность вместе с производительностью. Беда с тем, что от этого счастья о стабильном фреймрейте можно только мечтать, а панацея в виде fullscreen может на vsync сжирать время, которое ты вполне мог использовать. Наконец, беда с тем, что очень плохо понятно, во что и где ты уперт, и что именно нужно оптимизять. Отличить большой CPU hit от него же, но с добавкой от GPU – крайне тяжело. А средства дополнительной стабилизации – ухудшают общую производительность (в принципе, для физика ничего удивительного – факт измерения влияет на систему).
А еще вокруг этого любимый PC, где как известно, измерение любой достаточно большой части кода дрожит всегда.
А еще у DX крайне интимные отношения с GDI, из-за которых мистику поведения D3D с popup-иконками и анимациями в трее объяснить не может никто, включая саппорт вендоров.
В результате тыкаемся в темноте, не понимая где и куда утекает производительность. И еще хуже понимаем, что надо делать, чтобы она вернулась.

Единственный светлый лучик – последнее поколение тулзов NV, типа PerfHUD 4.0 и PerfKit. Да и то, надо еще научиться им верить.

Автор sim0nsays.