Почему этот вычислительный шейдер намного медленнее, чем вершинный?
Я исследую использование вычислительного шейдера для применения деформации кости к вершинам сетки, а не к вершинному шейдеру с выводом потока. Я обнаружил, что вычислительный шейдер выполняется намного медленнее, чем вершинный шейдер, но прежде чем списать его со счетов, я хочу убедиться, что я не делаю что-то не так.
С моими тестовыми данными из 100 000 вершин и 1000 кадров анимационных данных для 300 костей вершинный шейдер работает примерно за 0,22 мс, а вычислительный шейдер занимает 4 раза больше при 0,85 мс. Синхронизация выполняется с помощью запросов таймера API D3D (а не таймера процессора).
deform_structs.hlsl
struct Vertex {
float3 position : POSITION;
float3 normal : NORMAL;
float2 texcoord : TEXCOORD;
float3 tangent : TANGENT;
float4 color : COLOR;
};
struct BoneWeights {
uint index;
float weight;
};
StructuredBuffer<matrix> g_bone_array : register(t0);
Buffer<uint> g_bone_offsets : register(t1);
Buffer<uint> g_bone_counts : register(t2);
StructuredBuffer<BoneWeights> g_bone_weights : register(t3);
bone_deform_cs.hlsl
#include "deform_structs.hlsl"
StructuredBuffer<Vertex> g_input_vertex : register(t4);
RWStructuredBuffer<Vertex> g_output_vertex : register(u0);
[numthreads(64,1,1)]
void BoneDeformCS(uint id : SV_DispatchThreadID) {
Vertex vert = g_input_vertex[id.x];
uint offset = g_bone_offsets[id.x];
uint count = g_bone_counts[id.x];
matrix bone_matrix = 0;
for (uint i = offset; i < (offset + count); ++i) {
BoneWeights weight_info = g_bone_weights[i];
bone_matrix += weight_info.weight * g_bone_array[weight_info.index];
}
vert.position = mul(float4(vert.position,1), bone_matrix).xyz;
vert.normal = normalize(mul(vert.normal, (float3x3)bone_matrix));
vert.tangent = normalize(mul(vert.tangent, (float3x3)bone_matrix));
g_output_vertex[id.x] = vert;
}
bone_deform_vs.hlsl
#include "deform_structs.hlsl"
void BoneDeformVS(uint id : SV_VertexID, Vertex vsin, out Vertex vsout) {
uint offset = g_bone_offsets[id];
uint count = g_bone_counts[id];
matrix bone_matrix = 0;
for (uint i = offset; i < (offset + count); ++i) {
BoneWeights bone_info = g_bone_weights[i];
bone_matrix += bone_info.weight * g_bone_array[bone_info.index];
}
vsout.position = mul(float4(vsin.position,1), bone_matrix).xyz;
vsout.normal = normalize(mul(vsin.normal, (float3x3)bone_matrix));
vsout.tangent = normalize(mul(vsin.tangent, (float3x3)bone_matrix));
vsout.texcoord = vsin.texcoord;
vsout.color = vsin.color;
}
Сравнивая содержимое буферов после их запуска, они идентичны и содержат ожидаемые значения.
Я подозреваю, что, возможно, я неправильно выполняю вычислительный шейдер, порождая слишком много потоков? У меня есть номер, который я передаю Dispatch
неправильно? Поскольку это одномерный ряд данных, для меня имеет смысл использовать [numthreads(64,1,1)]
, Я пробовал различные значения от 32-1024. 64, кажется, приятное место, поскольку это минимум, необходимый для эффективного использования графических процессоров AMD. Тем не мение. Когда я звоню Dispatch
Прошу его выполнить (vertex_count / 64) + (vertex_count % 64 != 0) ? 1 : 0
, Для 100 000 вершин вызов заканчивается Dispatch(1563,1,1)
,
ID3D11ShaderResourceView * srvs[] = {bone_array_srv, bone_offset_srv,
bone_count_srv, bone_weights_srv,
cs_vertices_srv};
ID3D11UnorderedAccessView * uavs[] = {cs_output_uav};
UINT srv_count = sizeof(srvs) / sizeof(srvs[0]);
UINT uav_count = sizeof(uavs) / sizeof(uavs[0]);
UINT thread_group_count = vertex_count / 64 + (vertex_count % 64 != 0) ? 1 : 0;
context->CSSetShader(cs, nullptr, 0);
context->CSSetShaderResources(0, srv_count, srvs);
context->CSSetUnorderedAccessViews(0, uav_count, uavs);
context->Dispatch(thread_group_count, 1, 1);
И вот как выполняется вершинный шейдер:
ID3D11ShaderResourceView * srvs[] = {bone_array_srv, bone_offset_srv,
bone_count_srv, bone_weights_srv};
UINT srv_count = sizeof(srvs) / sizeof(srvs[0]);
UINT stride = 0;
UINT offset = 0;
context->GSSetShader(streamout_gs, nullptr, 0);
context->VSSetShader(vs, nullptr, 0);
context->VSSetShaderResources(0, srv_count, srvs);
context->SOSetTargets(1, &vs_output_buf, &offset);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_POINTLIST);
context->IASetInputLayout(vs_input_layout);
context->IASetVertexBuffers(0, 1, &vs_vertices, &stride, &offset);
context->Draw(vertex_count, 0);
Или ответ заключается лишь в том, что чтение из представления ресурсов шейдера и запись в неупорядоченное представление доступа намного медленнее, чем чтение из буфера вершин и запись в буфер вывода потока?
1 ответ
Я только учусь работать с вычислительными шейдерами, поэтому я не эксперт. Что касается расчета вашей кости, я уверен, что CS должен работать по крайней мере так же быстро, как VS. Интуиция говорит мне, что numthreads (64,1,1)
менее эффективен, чем что-то вроде numthreads (16,16,1)
, Таким образом, вы можете попробовать этот подход:
- Обрабатывайте линейный буфер так, как если бы он имел квадратичную разметку, а размеры x и y одинаковы
- Вычислить размер х / у как
size = ceil (sqrt (numvertices))
- Используйте диспат
ch(size / 16, size / 16)
в вашей программе иnumthreads (16,16,1)
в вашем файле hlsl - Выделите постоянный буфер, куда вы копируете
size
а такжеnumvertices
ценности - Вместо того, чтобы использовать
id.x
в качестве индекса вы рассчитываете свой (линейный) индекс какint index = id.y * size +id.x)
(возможно, id.xy также возможен в качестве индекса) В большинстве случаев
size * size
будет больше чемnumvertices
, так что вы получите больше потоков, чем вершин. Вы можете заблокировать эти дополнительные потоки, добавив условие в функцию hlsl:int index = id.y * size +id.x; if (index < numvertices) { .. // your code follows
Я надеюсь, что этот подход ускоряет ваши расчеты CS.
================ РЕДАКТИРОВАТЬ ==================
Мое предложение было основано на моих собственных временных тестах. Чтобы проверить мой случай, я повторил эти тесты с большим количеством вариаций параметров numthreads. Я вычисляю значение Мандельброта более 1034 x 827 = 855,118 пикселей. Вот результаты:
numthreads Dispatch groups threads/ total
x y fps x y group threads
4 4 240 259 207 53445 16 855118
8 8 550 129 103 13361 64 855118
16 16 600 65 52 3340 256 855118
32 32 580 32 26 835 1024 855118
64 1 550 16 827 13361 64 855118
256 1 460 4 827 3340 256 855118
512 1 370 2 827 1670 512 855118
Как вы можете видеть, самое подходящее место - numthreads(16,16,1) - создает такое же количество групп потоков (3340), что и numthreads(256,1,1), но производительность на 30% выше. Обратите внимание, что общее количество потоков (и должно быть) всегда одинаково! Мой графический процессор - ATI 7790.
================ РЕДАКТИРОВАТЬ 2 ==================
Чтобы глубже разобраться в вашем вопросе о скорости CS и VS, я еще раз просмотрел очень интересное видео на 9 канале (презентация PDC09, проведенная главным архитектором Microsoft Чейсом Бойдом о прямых вычислениях, см. Ссылку ниже). В этой презентации Бойд утверждает, что оптимизация расположения потоков (numthreads) может привести к двукратному увеличению пропускной способности.
Более интересным, однако, является часть его презентации (начиная с 40-й минуты), где он объясняет корреляцию между БПЛА и расположением памяти графического процессора ("Графика против вычислений ввода-вывода"). Я не хочу делать неверные выводы из заявлений Бойда, но, по крайней мере, представляется возможным, что шейдеры Compute, связанные с помощью БПЛА , имеют меньшую пропускную способность памяти, чем другие шейдеры с графическим процессором. Если бы это было правдой, у нас могло бы быть объяснение того факта, что БПЛА не могут быть связаны с VS, например (по крайней мере, в версии 11.0).
Поскольку эти шаблоны доступа к памяти также зависят от аппаратного обеспечения, вам следует обратиться к инженерам ATI / NVIDIA напрямую.
ЗАКЛЮЧЕНИЕ
Я собрал тонны информации об использовании CS, но не было ни малейшего указания на то, что CS может запускать тот же алгоритм медленнее, чем VS. Если это действительно так, вы обнаружили что-то, что имеет значение для всех людей, которые используют прямые вычисления.