【问题标题】:How to optimize "u[0]*v[0] + u[2]*v[2]" code line with SSE or GLSL如何使用 SSE 或 GLSL 优化“u[0]*v[0] + u[2]*v[2]”代码行
【发布时间】:2012-06-22 21:58:51
【问题描述】:

我有以下功能(来自开源项目"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 CPU 性能测试运行它,它告诉我在这个函数u[0]*v[0] + u[2]*v[2] 的所有重铸代码库代码行中 CPU 最热。

我如何 CPU 优化(例如通过 SSE 或 GLSL like GLM (if it is easier or faster and appropriate in such case))这一行?

编辑:调用出现的上下文:

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;

【问题讨论】:

  • 我怀疑这个叶子函数可以进一步优化,因为它只进行 3 次 FP 操作。可能在调用站点进行优化。
  • 如果这个函数被调用了很多,如果有意义的话尝试使用OpenMP。
  • IMO 这不是正确的方法。 SSE 并不是真正用于水平操作。有一些水平指令,但它们(几乎)都很慢。使用 SSE,一次计算 4 件事情几乎总是比尝试以 4 倍的速度做一件事情要好。
  • 我添加了更多信息。显然代码可以使用 SSE 进行优化,同样明显的是数据布局对于这种工作来说不是很聪明,并且会从重组中受益。
  • u[0]、v[0]、u[2] 和 v[2] 的值范围是多少?另外,您需要的最低精度是多少(即可以接受多少粒度误差)?

标签: c++ c optimization sse glm-math


【解决方案1】:

在纸上尝试了一些东西之后,我想出了一些可能对你有用的东西。它是 SSE 中函数的适当并行化/矢量化实现。

但它确实需要重新组织数据,因为我们将同时对 4 个三角形进行并行处理。

我将逐步分解它并在这里和那里使用指令名称,但请使用 C 内在函数(_mm_load_ps()、_mm_sub_ps() 等,它们在 VC 中的 xmmintrin.h 中)——当我说话时的寄存器,这只是意味着 __m128。

第 1 步。

我们根本不需要 Y 坐标,因此我们设置了指向 X 和 Z 对的指针。每次调用至少提供 4 对(即总共 4 个三角形)。 我将每个 X 和 Z 对称为一个顶点。

第 2 步。

使用 MOVAPS(要求指针对齐到 16 位)将每个指针指向的前两个顶点加载到寄存器中。

a 加载的寄存器将如下所示: [ a0.x, a0.z, a1.x, a1.z ]

第 3 步。

现在,使用单个减法指令,您可以一次计算 2 个顶点的增量(您的 v0v1v2) .

计算 v0v1v2 不仅针对前 2 个三角形,还针对后 2 个三角形! 正如我所说,您应该为每个输入提供总共 4 个顶点或 8 个浮点数。只需针对该数据重复第 2 步和第 3 步

现在我们有 2 对 vx 寄存器,每对包含 2 个三角形的结果。我将它们称为 vx_0(第一对)和 vx_1(第二对),其中 x 是从 0 到 2。

第 4 步。

点积。为了并行化重心计算(稍后),我们需要 4 个三角形中每个三角形的每个点积的结果,在 1 个单个寄存器中。

因此,例如,在您计算 dot01 的地方,我们将执行相同的操作,但一次计算 4 个三角形。每个 v 寄存器包含 2 个向量的结果,因此我们首先将它们相乘。

假设 uv -- 您的点积函数中的参数 -- 现在是 v0_0v1_0(如计算dot01):

uv 相乘得到: [ (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 步加载的内容:a0a1

如果到现在还感觉模糊,拿起一张纸从头开始写。

接下来,仍然是相同的点积,对 v0_1v1_1 进行乘法运算(即第二对三角形)。

现在我们有 2 个寄存器,每个寄存器有 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:此时我们已经为所有 4 个三角形得到了这个特殊的点积!

**现在对所有点积重复此过程:dot00 等等。 **

第 5 步。

最后一个计算(就我所提供的代码所见)是重心的东西。这是代码中的 100% 标量计算。但是您现在的输入不是标量点积结果(即单个浮点数),它们是 SSE 向量/寄存器,4 个三角形中的每一个都有一个点积。

因此,如果您通过使用对所有 4 个浮点数进行操作的并行 SSE 操作将其平面化,您最终将得到 1 个寄存器(或结果)携带 4 个高度,每个三角形 1 个高度。

由于我的午休时间已经过去了,所以我不会详细说明这一点,但鉴于我给出的设置/想法,这是最后一步,应该不难弄清楚。

我知道这个想法有点牵强,需要对它上面的代码的一些热爱,也许还有一些用铅笔和纸的质量时间,但它会很快(如果你甚至可以在之后添加 OpenMP '想要)。

祝你好运:)

(请原谅我的模糊解释,如果需要,我可以编写函数 =))

更新

我已经编写了一个实现,但它并没有像我预期的那样进行,主要是因为 Y 组件超出了您最初粘贴在问题中的代码段(我查了一下)。我在这里所做的不仅是要求您将所有点重新排列为 XZ 对并每 4 个输入它们,而且还输入 3 个指针(用于点 A、B 和 C)以及 4 个三角形中每个三角形的 Y 值。从当地的角度来看,这是最快的。我仍然可以对其进行修改,以减少被调用方的干扰,但请让我知道什么是可取的。

然后是免责声明:这段代码非常简单(我发现它在 SSE 方面与编译器配合得很好......他们可以根据需要重新组织,x86/x64 CPU 也参与其中) .还有命名,这不是我的风格,也不是任何人的风格,只要你认为合适就行。

希望它有所帮助,如果没有,我会很乐意再看一遍。如果这是一个商业项目,我猜也可以选择让我加入;)

不管怎样,我已经把它放在了 pastebin 上:http://pastebin.com/20u8fMEb

【讨论】:

  • 炉排展开,启动函数!)
  • 我犯了一个致命的错误——Y 坐标用于稍后进行插值。该死的。不管怎样,我会努力解决的。
【解决方案2】:

您可以使用 SSE 指令实现单点积,但结果不会比现在编写的代码快得多(甚至可能更慢)。您的重写可能会破坏有助于当前版本的编译器优化。

为了从使用 SSE 或 CUDA 重写中获得任何好处,您必须优化调用该点积的循环。对于 CUDA 来说尤其如此,因为执行一个点积的开销会很大。只有将数千个向量发送到 GPU 以计算数千个点积,才能看到加速。同样的想法也适用于 CPU 上的 SSE,但您可能会看到在较少数量的操作上有所改进。不过,它仍将不仅仅是一个点积。

最容易尝试的可能是g++ -ftree-vectorize。 GCC 将能够内联您的小函数,然后尝试为您优化循环(实际上它可能已经是,但没有 SSE 指令)。树矢量化器将尝试自动执行您建议手动执行的操作。它并不总是成功的。

【讨论】:

  • 也不要忘记 -msse 或 -msse2 (etc)。
【解决方案3】:

SSE 指令用于优化处理以整数或浮点数表示的大块数据的算法。典型的大小是需要以某种方式处理的数以百万计的数字。优化只处理四个(或二十个)标量的函数是没有意义的。您从 SSE 获得的收益可能会因函数调用开销而损失。一次函数调用处理的合理数字至少为千。通过使用 SSE 内在函数,您可能会获得巨大的性能提升。但很难根据您提供的信息为您提供适合您需求的具体建议。您应该编辑您的问题并提供更高级的问题视图(位于调用堆栈更深处的函数)。例如,每秒调用 dtClosestHeightPointTriangle 方法多少次并不明显?这个数字对于客观判断向 SSE 过渡是否具有实用价值至关重要。数据的组织也很重要。理想情况下,您的数据应存储在尽可能少的线性内存段中,以有效利用 CPU 的缓存子系统。

【讨论】:

  • 如果你经常非常处理小块的浮点数,那么向量化它当然是有意义的。这更难,因为通常会有一些改组(例如,获得单个标量结果),而且这比大规模数组的总和占总时间的比例要大得多。因此,与自动矢量化相比,更可能需要手动矢量化处理少量数据的热函数。
【解决方案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 的平台都应该支持它。

有几点需要注意:

  • 您需要使用 XMLoadFloat3 方法将您的浮点数加载到 XMVECTOR 中;这会将未对齐的 float3 加载到 __m128 结构中。
  • 您可能不会从该 SSE 代码(配置文件!!)中看到任何性能改进,因为将未对齐的浮点数加载到 SSE 寄存器中会降低性能。
  • 这是算法到 SSE 的强力转换,如果您比我更聪明并实际尝试理解算法并实现 SSE 友好版本,您会获得更好的运气。
  • 通过将整个应用程序转换为使用 XNA Math/SSE 代码而不是仅仅使用一小部分代码,您将获得更好的运气。至少强制使用对齐的向量类型(XMFLOAT3A 或 struct __declspec(align(16)) myvectortype { };)。
  • 不鼓励直接 SSE 汇编,尤其是在 x64 中,有利于内部函数。

【讨论】:

    猜你喜欢
    • 2015-06-13
    • 1970-01-01
    • 2014-07-20
    • 1970-01-01
    • 1970-01-01
    • 2015-10-02
    • 2019-02-28
    • 2020-01-16
    • 1970-01-01
    相关资源
    最近更新 更多