InterlockedAdd HLSL потенциальная оптимизация

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

В некоторой информации, которую я нашел в Интернете, говорится, что атомарные добавления могут создавать серьезные проблемы с конкуренцией: https://developer.nvidia.com/blog/cuda-pro-tip-optimized-filtering-warp-aggregated-atomics/

Конечно, статья выше написана для CUDA (тоже немного устарела, датируется 2014 годом), тогда как меня интересует HLSL InterlockedAdd. С этой целью я написал фиктивный шейдер HLSL для Unity (насколько мне известно, скомпилированный в d3d11 через FXC), где я вызываю InterlockedAdd для одного глобального атомарного счетчика, так что добавленное значение всегда равно одинакова для всех заштрихованных фрагментов. Рассматриваемый фрагмент (запущенный в http://shader-playground.timjones.io/), скомпилированный через FXC, оптимизация 3 уровня, модель затенения 5.0):

**HLSL**:
RWStructuredBuffer<int> counter : register(u1);
void PSMain()
{
    InterlockedAdd(counter[0], 1);
}
----
**Assembly**:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_uav_structured u1, 4
atomic_iadd u1, l(0, 0, 0, 0), l(1)
ret 

Затем я немного изменил код и вместо того, чтобы всегда добавлять некоторое постоянное значение, теперь я добавляю значение, которое варьируется в зависимости от фрагмента, поэтому что-то вроде этого:

**HLSL**:
RWStructuredBuffer<int> counter : register(u1);
void PSMain(float4 pixel_pos : SV_Position)
{
    InterlockedAdd(counter[0], int(pixel_pos.x));
}
----
**Assmebly**:
ps_5_0
dcl_globalFlags refactoringAllowed
dcl_uav_structured u1, 4
dcl_input_ps_siv linear noperspective v0.x, position
dcl_temps 1
ftoi r0.x, v0.x
atomic_iadd u1, l(0, 0, 0, 0), r0.x
ret 

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

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

Итак, мой вопрос: происходит ли какая-то оптимизация при использовании HLSL InterlockedAdd на одном глобальном атомарном счетчике, чтобы добавленное значение было постоянным? Возможно ли, что драйвер графического процессора может как-то изменить код?

Характеристики системы:

  • NVIDIA Quadro P4000
  • Windows 10
  • Единство 2019.4

person haykoandri    schedule 10.12.2020    source источник


Ответы (1)


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

В первой версии 64-пиксельный волновой фронт выполнял код как одиночный simd InterlockedAdd‹64›(counter[0], 1); или даже оптимизировать его в InterlockedAdd(counter[0], 64); Во втором примере он превращается в серию последовательных, не симд-аддов и становится дороже в 64 раза.

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

person George Davison    schedule 24.03.2021