如何使用SSE或GLSL优化“u [0] * v [0] + u [2] * v [2]”代码行

时间:2012-06-20 17:37:50

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

我有以下功能(来自开源项目"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;

4 个答案:

答案 0 :(得分:21)

在纸上尝试了一些事情后,我想出了可能为你工作的东西。它是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个顶点的增量( v0 v1 v2

计算 v0 v1 v2 不仅适用于前两个三角形,还适用于后两个三角形! 正如我所说,每个输入应该提供总共4个顶点或8个浮点数。只需重复该数据的第2步和第3步

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

第4步。

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

例如,在您计算 dot01 的地方,我们会做同样的事情,但同时对于4个三角形。每个 v -register包含2个向量的结果,因此我们首先将它们相乘。

让我们说你的点积函数中的 u v - 参数 - 现在是 v0_0 v1_0 < / i>(计算 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 ,即第二对三角形)进行乘法运算。 / p>

现在我们有2个寄存器,2个X&amp;每个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&amp;来自我们的第一个寄存器的Z对,来自第二个寄存器的Z对将它们加在一起并将它们存储在目标寄存器的第一,第二,第三和第四个组件中。 Ergo:此时我们已经为所有4个三角形获得了这个特殊的点积!

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

第5步。

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

因此,如果你通过使用对所有4个浮点运算的并行SSE运算来展平矢量化,那么你最终将得到1个寄存器(或结果),其中包含4个高度,每个三角形为1个。

由于我的午休时间已经过去了,因为我不会拼出这个,但考虑到设置/想法我已经给出了这是最后一步,不应该很难弄清楚。

我知道这个想法有点牵强,需要一些爱好来自它上面的代码,也许需要一些有铅笔和纸张的质量时间,但它会很快(如果你和你甚至可以添加OpenMP) #39; d like)。

祝你好运:)

(并且原谅我的模糊解释,如果需要,我可以掀起函数=))

<强>更新

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

然后是一个免责声明:这个代码很简单就像地狱一样(我发现它与SSE方面的编译器工作得很好......他们可以重新组织,因为看起来合适,x86 / x64 CPU占用它们的份额太)。也是命名,它不是我的风格,它不是任何人,只是用你认为合适的方式做。

希望它有所帮助,如果不是,我很乐意再次讨论它。如果这是一个商业项目,那么我也可以选择让我参与其中;)

无论如何,我已将它放在pastebin上:http://pastebin.com/20u8fMEb

答案 1 :(得分:4)

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

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

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

答案 2 :(得分:3)

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

答案 3 :(得分:2)

您要求提供算法的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是微软的一个很好的矢量化跨平台数学库;我通过导入sal.h标头在OS X上使用它(我不确定那里的许可问题,所以要注意)。
实际上,任何支持内在函数SSE 的平台都应支持它。

要注意的几件事情:

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