Как оптимизировать "u [0] * v [0] + u [2] * v [2]" строка кода с SSE или GLSL

У меня есть следующая функция (из проекта с открытым исходным кодом "recast navigation" ):

/// Derives the dot product of two vectors on the xz-plane. (@p u . @p v)
///  @param[in]     u       A vector [(x, y, z)]
///  @param[in]     v       A vector [(x, y, z)]
/// @return The dot product on the xz-plane.
///
/// The vectors are projected onto the xz-plane, so the y-values are ignored.
inline float dtVdot2D(const float* u, const float* v)
{
    return u[0]*v[0] + u[2]*v[2];
}

Я проверил его с помощью теста производительности процессора VS2010, и он показал мне, что во всей перекодированной кодовой базе кода в этой функции u[0]*v[0] + u[2]*v[2] наиболее горячий процессор.

Как я могу оптимизировать CPU (через SSE или GLSL, например GLM (если это проще или быстрее и подходит в таком случае)) эта строка?

Изменить: контекст, в котором отображаются вызовы:

bool dtClosestHeightPointTriangle(const float* p, const float* a, const float* b, const float* c, float& h) {
    float v0[3], v1[3], v2[3];
    dtVsub(v0, c,a);
    dtVsub(v1, b,a);
    dtVsub(v2, p,a);

    const float dot00 = dtVdot2D(v0, v0);
    const float dot01 = dtVdot2D(v0, v1);
    const float dot02 = dtVdot2D(v0, v2);
    const float dot11 = dtVdot2D(v1, v1);
    const float dot12 = dtVdot2D(v1, v2);

    // Compute barycentric coordinates
    const float invDenom = 1.0f / (dot00 * dot11 - dot01 * dot01);
    const float u = (dot11 * dot02 - dot01 * dot12) * invDenom;
    const float v = (dot00 * dot12 - dot01 * dot02) * invDenom;

Ответ 1

Попробовав несколько вещей на бумаге, я придумал что-то, что может сработать для вас. Это правильно распараллелированная/векторная реализация функции в SSE.

Однако для этого требуется реорганизация данных, потому что мы будем выполнять параллельную обработку для четырех треугольников одновременно.

Я разбиваю его по шагам и использую имена инструкций здесь и там, но, пожалуйста, используйте C intrinsics (_mm_load_ps(), _mm_sub_ps() и др., они находятся в xmmintrin.h в VC) - когда я говорю это означает только __m128.

ШАГ 1.

Нам вообще не нужна координата Y, поэтому мы устанавливаем указатели на пары X и Z. Поставляем по крайней мере 4 пары (т.е. 4 треугольника) за вызов. Я назову каждую пару X и Z вершиной.

ШАГ 2.

Используйте MOVAPS (требуется, чтобы указатели были выровнены по 16 бит) для загрузки первых двух вершин, на которые указатель указывал каждый указатель на регистры.

Регистр, загруженный из a, будет выглядеть так: [a0.x, a0.z, a1.x, a1.z]

ШАГ 3.

Теперь, используя одну инструкцию вычитания, вы можете вычислить дельта (ваши v0, v1, v2) для двух вершин сразу.

Вычислить v0, v1 и v2 не только для первых двух треугольников, но и для последних 2! Как я сказал, вы должны предоставить в общей сложности 4 вершины или 8 поплавков на каждый вход. Просто повторите шаги 2 и 3 для этих данных.

Теперь у нас есть 2 пары регистров vx, каждая пара содержит результат для 2 треугольников. Я буду называть их vx_0 (первая пара) и vx_1 (вторая пара), где x - от 0 до 2.

ШАГ 4.

Точечные продукты. Чтобы распараллелить барицентрический расчет (позже), нам нужен результат каждого точечного произведения для каждого из четырех треугольников в одном единственном регистре.

Итак, где вы бы вычислили dot01, например, мы сделаем то же самое, но за 4 треугольника сразу. Каждый v-регистр содержит результат для 2 векторов, поэтому мы начинаем с их умножения.

Скажем, что u и v - параметры в вашей функции точечного произведения - теперь v0_0 и v1_0 (как рассчитать dot01):

Умножьте u и v, чтобы получить: [(v0_0.x0) * (v1_0.x0), (v0_0.z0) * (v1_0.z0), (v0_0.x1) * (v1_0.x1), (v0_0. z1) * (v1_0.z1)]

Это может показаться запутанным из-за .x0/.z0 и .x1/.z1, но посмотрите, что было загружено на шаге 2: a0, a1.

Если теперь это все еще кажется нечетким, возьмите лист бумаги и напишите с самого начала.

Далее, все же для одного и того же точечного произведения сделайте умножение для v0_1 и v1_1 (, т.е. вторая пара треугольников).

Теперь у нас есть 2 регистра с двумя парами X и Z каждый (всего 4 вершины), умноженные и готовые для добавления вместе, чтобы сформировать 4 отдельных точечных произведения. SSE3 имеет инструкцию для этого, и он называется HADDPS:

xmm0 = [A, B, C, D] xmm1 = [E, F, G, H]

HADDPS xmm0, xmm1 делает следующее:

xmm0 = [A + B, C + D, E + F, G + H]

Он возьмет пары X и Z из нашего первого регистра, те из второго, добавят их вместе и сохранят в первом, втором, третьем и четвертом компонентах регистра назначения. Ergo: на данный момент у нас есть этот точечный продукт для всех четырех треугольников!

** Теперь повторите этот процесс для всех точечных продуктов: dot00 и так далее. **

ШАГ 5.

Последний расчет (насколько я мог видеть по прилагаемому коду) - это барицентрический материал. Это 100% скалярный расчет в вашем коде. Но ваши входы теперь не являются результатами скалярных точечных продуктов (т.е. Одиночными поплавками), они представляют собой векторы/регистры SSE с точечным продуктом для каждого из четырех треугольников.

Итак, если вы выровняете вектор, используя параллельные операции SSE, которые работают на всех 4 поплавках, вы в конечном итоге получите 1 регистр (или результат), несущий 4 высоты, по 1 для каждого треугольника.

Так как мой перерыв на обед продлился, я не собираюсь писать это, но учитывая настройку/идею, которую я дал, это последний шаг, и его не должно быть трудно понять.

Я знаю, что эта идея немного растянута и требует некоторой любви от кода, который находится над ним, и, возможно, какое-то время с карандашом и бумагой, но он будет быстрым (и вы даже можете добавить OpenMP впоследствии, если вы 'd like).

Удачи:)

(и простите мое нечеткое объяснение, я могу взломать функцию, если нужно be =))

UPDATE

Я написал реализацию, и она не пошла так, как я ожидал, главным образом потому, что компонент Y был вовлечен за кусок кода, который вы вначале вставляли в свой вопрос (я искал его). То, что я здесь сделал, - это не просто попросить вас переставить все точки в пары XZ и подать их на 4, но также указать 3 указателя (для точек A, B и C) с значениями Y для каждого из четырех треугольников. С местной точки зрения это происходит быстрее всего. Я все еще могу изменить его, чтобы потребовать меньше навязчивых изменений со стороны вызываемого абонента, но, пожалуйста, дайте мне знать, что желательно.

Тогда отказ от ответственности: этот код прост, как черт (что-то, что я нашел очень хорошо работающим с компиляторами с точки зрения SSE... они могут реорганизоваться по мере того, как подходят, а процессоры x86/x64 тоже принимают в этом участие), Именование, это не мой стиль, это не кто-то, просто сделайте с ним то, что вы сочтете нужным.

Надеюсь, что это поможет, и если нет, я с удовольствием перейду к нему снова. И если это коммерческий проект, есть возможность получить меня на борту, я думаю;)

В любом случае, я положил его на pastebin: http://pastebin.com/20u8fMEb

Ответ 2

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

Чтобы воспользоваться преимуществами перезаписи, используя SSE или CUDA, вам необходимо оптимизировать цикл, который вызывает этот точечный продукт. Это особенно актуально для CUDA, где накладные расходы на выполнение одного точечного продукта будут огромными. Вы могли видеть только ускорение, если вы отправили тысячи векторов в графический процессор для вычисления тысяч точечных продуктов. Эта же идея относится к SSE на CPU, но вы можете увидеть улучшение по сравнению с меньшим количеством операций. Тем не менее, это будет более чем один точечный продукт.

Проще всего попробовать g++ -ftree-vectorize. GCC сможет встроить вашу небольшую функцию, а затем попытаться оптимизировать цикл для вас (на самом деле это уже есть, но без инструкций SSE). Инструмент вектора деревьев попытается автоматически выполнить то, что вы предлагаете сделать вручную. Это не всегда успешно.

Ответ 3

Команды SSE предназначены для оптимизации алгоритмов, которые обрабатывают большие блоки данных, представляемые в виде целых чисел или чисел с плавающей запятой. Типичные размеры - миллионы и миллиарды чисел, которые необходимо каким-то образом обработать. Не имеет смысла оптимизировать функцию, которая обрабатывает только четыре (или двадцать) скаляров. Что вы получите с помощью SSE, вы можете потерять с накладными функциями. Разумное количество номеров, обрабатываемых одним вызовом функции, составляет не менее тысячи. Возможно, вы можете получить огромный прирост производительности, используя встроенные функции SSE. Но его трудно дать вам конкретные рекомендации, адаптированные к вашим потребностям на основе предоставленной вами информации. Вы должны отредактировать свой вопрос и предоставить более высокий уровень представления вашей проблемы (функции расположены глубже в вашем стоп-лоте). Например, неясно, сколько раз вызывается метод dtClosestHeightPointTriangle в секунду? Это число имеет решающее значение для объективного суждения, если бы переход к SSE имел бы практическую ценность. Организация данных также очень важна. В идеале ваши данные должны храниться как можно меньше линейных сегментов памяти для эффективного использования подсистемы кэша ЦП.

Ответ 4

Вы попросили версию SSE вашего алгоритма, вот он:

// Copied and modified from xnamathvector.inl
XMFINLINE XMVECTOR XMVector2DotXZ
(
    FXMVECTOR V1, 
    FXMVECTOR V2
)
{
#if defined(_XM_NO_INTRINSICS_)

    XMVECTOR Result;

    Result.vector4_f32[0] =
    Result.vector4_f32[1] =
    Result.vector4_f32[2] =
    Result.vector4_f32[3] = V1.vector4_f32[0] * V2.vector4_f32[0] + V1.vector4_f32[2] * V2.vector4_f32[2];

    return Result;

#elif defined(_XM_SSE_INTRINSICS_)
    // Perform the dot product on x and z
    XMVECTOR vLengthSq = _mm_mul_ps(V1,V2);
    // vTemp has z splatted
    XMVECTOR vTemp = _mm_shuffle_ps(vLengthSq,vLengthSq,_MM_SHUFFLE(2,2,2,2));
    // x+z
    vLengthSq = _mm_add_ss(vLengthSq,vTemp);
    vLengthSq = _mm_shuffle_ps(vLengthSq,vLengthSq,_MM_SHUFFLE(0,0,0,0));
    return vLengthSq;
#else // _XM_VMX128_INTRINSICS_
#endif // _XM_VMX128_INTRINSICS_
}

bool dtClosestHeightPointTriangle(FXMVECTOR p, FXMVECTOR a, FXMVECTOR b, FXMVECTOR c, float& h)
{
    XMVECTOR v0 = XMVectorSubtract(c,a);
    XMVECTOR v1 = XMVectorSubtract(b,a);
    XMVECTOR v2 = XMVectorSubtract(p,a);

    XMVECTOR dot00 = XMVector2DotXZ(v0, v0);
    XMVECTOR dot01 = XMVector2DotXZ(v0, v1);
    XMVECTOR dot02 = XMVector2DotXZ(v0, v2);
    XMVECTOR dot11 = XMVector2DotXZ(v1, v1);
    XMVECTOR dot12 = XMVector2DotXZ(v1, v2);

    // Compute barycentric coordinates
    XMVECTOR invDenom = XMVectorDivide(XMVectorReplicate(1.0f), XMVectorSubtract(XMVectorMultiply(dot00, dot11), XMVectorMultiply(dot01, dot01)));

    XMVECTOR u = XMVectorMultiply(XMVectorSubtract(XMVectorMultiply(dot11, dot02), XMVectorMultiply(dot01, dot12)), invDenom);
    XMVECTOR v = XMVectorMultiply(XMVectorSubtract(XMVectorMultiply(dot00, dot12), XMVectorMultiply(dot01, dot02)), invDenom);
}

XMVector2Dot берется из xnamathvector.inl, я переименовал его и модифицировал для работы с координатами X/Z.

XNAMath - отличная векторная кросс-платформенная математическая библиотека от Microsoft; Я использую его также в OS X, импортируя заголовок sal.h(я не уверен в проблеме лицензирования, поэтому следите).
Фактически, любая платформа, поддерживающая встроенный SSE, должна поддерживать ее.

Несколько вещей, на которые нужно следить:

  • Вам необходимо загрузить ваши поплавки в XMVECTOR с помощью метода XMLoadFloat3; это будет загружать un-aligned float3 в структуру __m128.
  • Вероятно, вы не увидите улучшения производительности из этого кода SSE (профиль!), поскольку существует ограничение производительности для загрузки не выровненных поплавков в регистры SSE.
  • Это преобразование алгоритма в SSE с грубой силой, вам будет лучше повезло, умнее меня и на самом деле попытаться понять алгоритм и реализовать дружественную версию SSE.
  • Вам будет лучше повезти, преобразовая все приложение, чтобы использовать XNA Math/SSE-код, а не только эту небольшую часть. По крайней мере, принудительно применяйте ориентированные векторные типы (XMFLOAT3A или struct __declspec (align (16)) myvectortype {};).
  • Прямая сборка SSE не рекомендуется, особенно в x64, в пользу встроенных функций.