SSE/NEON 查表优化

Posted

技术标签:

【中文标题】SSE/NEON 查表优化【英文标题】:SSE/NEON table lookup optimization 【发布时间】:2013-07-18 13:21:42 【问题描述】:

我有以下查找和插值代码要优化。 (128号浮台) 它将与 Windows 上的 Intel 编译器、OSX 上的 GCC 和 neon OSX 上的 GCC 一起使用。

for(unsigned int i = 0 ; i < 4 ; i++)

    const int iIdx = (int)m_fIndex[i];
    const float frac = m_fIndex - iIdx;
    m_fResult[i] = sftable[iIdx].val + sftable[iIdx].val2 * frac;

我用 sse/neon 对所有内容进行了 vecorized。 (宏转换成 sse/neon 指令)

VEC_INT iIdx = VEC_FLOAT2INT(m_fIndex);
VEC_FLOAT frac = VEC_SUB(m_fIndex ,VEC_INT2FLOAT(iIdx);
m_fResult[0] = sftable[iIdx[0]].val2;
m_fResult[1] = sftable[iIdx[1]].val2;
m_fResult[2] = sftable[iIdx[2]].val2;
m_fResult[3] = sftable[iIdx[3]].val2;
m_fResult=VEC_MUL( m_fResult,frac);
frac[0] = sftable[iIdx[0]].val1;
frac[1] = sftable[iIdx[1]].val1;
frac[2] = sftable[iIdx[2]].val1;
frac[3] = sftable[iIdx[3]].val1;
m_fResult=VEC_ADD( m_fResult,frac);

我认为表访问和移动到对齐的内存是这里真正的瓶颈。 我不擅长汇编但有很多 unpcklps 和 mov:

10026751  mov         eax,dword ptr [esp+4270h] 
10026758  movaps      xmm3,xmmword ptr [eax+16640h] 
1002675F  cvttps2dq   xmm5,xmm3 
10026763  cvtdq2ps    xmm4,xmm5 
10026766  movd        edx,xmm5 
1002676A  movdqa      xmm6,xmm5 
1002676E  movdqa      xmm1,xmm5 
10026772  psrldq      xmm6,4 
10026777  movdqa      xmm2,xmm5 
1002677B  movd        ebx,xmm6 
1002677F  subps       xmm3,xmm4 
10026782  psrldq      xmm1,8 
10026787  movd        edi,xmm1 
1002678B  psrldq      xmm2,0Ch 
10026790  movdqa      xmmword ptr [esp+4F40h],xmm5 
10026799  mov         ecx,dword ptr [eax+edx*8+10CF4h] 
100267A0  movss       xmm0,dword ptr [eax+edx*8+10CF4h] 
100267A9  mov         dword ptr [eax+166B0h],ecx 
100267AF  movd        ecx,xmm2 
100267B3  mov         esi,dword ptr [eax+ebx*8+10CF4h] 
100267BA  movss       xmm4,dword ptr [eax+ebx*8+10CF4h] 
100267C3  mov         dword ptr [eax+166B4h],esi 
100267C9  mov         edx,dword ptr [eax+edi*8+10CF4h] 
100267D0  movss       xmm7,dword ptr [eax+edi*8+10CF4h] 
100267D9  mov         dword ptr [eax+166B8h],edx 
100267DF  movss       xmm1,dword ptr [eax+ecx*8+10CF4h] 
100267E8  unpcklps    xmm0,xmm7 
100267EB  unpcklps    xmm4,xmm1 
100267EE  unpcklps    xmm0,xmm4 
100267F1  mulps       xmm0,xmm3 
100267F4  movaps      xmmword ptr [eax+166B0h],xmm0 
100267FB  mov         ebx,dword ptr [esp+4F40h] 
10026802  mov         edi,dword ptr [esp+4F44h] 
10026809  mov         ecx,dword ptr [esp+4F48h] 
10026810  mov         esi,dword ptr [esp+4F4Ch] 
10026817  movss       xmm2,dword ptr [eax+ebx*8+10CF0h] 
10026820  movss       xmm5,dword ptr [eax+edi*8+10CF0h] 
10026829  movss       xmm3,dword ptr [eax+ecx*8+10CF0h] 
10026832  movss       xmm6,dword ptr [eax+esi*8+10CF0h] 
1002683B  unpcklps    xmm2,xmm3 
1002683E  unpcklps    xmm5,xmm6 
10026841  unpcklps    xmm2,xmm5 
10026844  mulps       xmm2,xmm0 
10026847  movaps      xmmword ptr [eax+166B0h],xmm2

在分析时,在 win 上使用 sse 版本并没有太多好处。

您对如何改进有什么建议吗? 预计会有 neon/gcc 的副作用吗?

目前我考虑只对第一部分进行 vecorized 并在循环中执行 tablereadout 和插值,希望它能从编译器优化中受益。

【问题讨论】:

单看,第一组代码的指令集似乎很长。 为此,AVX2(! - 仅限 Haswell)已获得 VGATHER 系列指令,用于在大型(内存中)表中查找。在 Intel 上,您可以通过 SSE/AVX 中的 shuffle 进行 small(微小)表查找,但前提是您的表中没有比您可以放入一个 SSE/AVX regs 更多的条目(因此对于 float,没有超过 4/8)。 ARM NEON 可以通过VTBL 执行相同的操作(并且该表可能包含在最多四个霓虹灯寄存器中,因此同样是八个浮点数)。 不幸的是,我需要最大的兼容性,所以我没有必要为 haswell 进行优化。 【参考方案1】:

操作系统?那么它就和NEON无关了。

顺便说一句,NEON 无论如何都无法处理这么大的 LUT。 (关于这件事我不知道SSE)

首先验证 SSE 是否可以处理这种大小的 LUT,如果可以,我建议使用不同的编译器,因为 GCC 往往会从内部函数中提取内部函数。

【讨论】:

我只是想明确一下代码将在各种平台上运行,并且它将包括我在 osx 上编译的霓虹矢量代码。【参考方案2】:

这是我见过的最糟糕的编译器代码生成(假设启用了优化器)。值得提交一个针对 GCC 的错误。

主要问题:

分别为每个查找加载 valval2。 将 valval2 的索引分别获取到 GPR 中。 将索引向量写入堆栈,然后将它们加载到 GPR 中。

为了让编译器生成更好的代码(每个表格行加载一次),您可能需要像加载双精度一样加载每个表格行,然后将该行转换为两个浮点数的向量并混合行得到齐次向量。在 NEON 和 SSE 上,这应该只需要 4 次加载和 3 或 4 次拆包(比目前的 8 次加载 + 6 次拆包要好得多)。

摆脱多余的堆栈流量可能更难。确保优化器已打开。修复多重加载问题将使堆栈流量减半,因为您只会生成每个索引一次,但要完全摆脱它,可能需要编写程序集而不是内在函数(或使用更新的编译器版本)。

【讨论】:

另外,更改来源。责备编译器进行不可避免的(重新加载),因为你没有明确(足够)你下面的内存无法更改是不公平的...... @FrankH.: (假设表没有标记为 volatile 并且 m_fResult 是本地的,几乎可以肯定是这种情况)编译器被允许(并且应该)假设表条目在它下面不会改变。也就是说,是的,源代码可以写得更干净(我在回答中建议了如何做到这一点)。 m_fResult 是这样,但 sftable[] 本身是 NOT @FrankH.:只有从sftablefrac 的负载之间的存储,我们可以看到是本地的,而m_fResult,我假设是本地的。因此,编译器可能假定它们不给 sftable 取别名,因此 sftable 中的值在代码序列执行期间不会更改,除非将 sftable 声明为 volatile(不太可能)。 谢谢。我意识到 m_fResult 不是本地的,所以帽子是错误组装的原因之一。现在好多了,但我认为并不完美。我会看看我是否可以给编译器更多关于 sftable 受到限制的提示。程序集是由 intelcompiler xe13 btw 创建的。【参考方案3】:

编译器在这里创建“时髦”代码(需要大量重新加载)的原因之一是,为了正确起见,它必须假设 sftable[] 数组中的数据可能会改变 .为了使生成的代码更好,请将其重组为:

VEC_INT iIdx = VEC_FLOAT2INT(m_fIndex);
VEC_FLOAT frac = VEC_SUB(m_fIndex ,VEC_INT2FLOAT(iIdx);
VEC_FLOAT fracnew;

// make it explicit that all you want is _four loads_
typeof(*sftable) tbl[4] = 
    sftable[iIdx[0]], sftable[iIdx[1]], sftable[iIdx[2]], sftable[iIdx[3]]
;

m_fResult[0] = tbl[0].val2
m_fResult[1] = tbl[1].val2;
m_fResult[2] = tbl[2].val2;
m_fResult[3] = tbl[3].val2;
fracnew[0] = tbl[0].val1;
fracnew[1] = tbl[1].val1;
fracnew[2] = tbl[2].val1;
fracnew[3] = tbl[3].val1;

m_fResult=VEC_MUL( m_fResult,frac);
m_fResult=VEC_ADD( m_fResult,fracnew);
frac = fracnew;

使用内部函数可能是有意义的(由于 sftable[] 中的 交错 布局),因为向量浮点数组 fResultfrac 很可能都可以从 @ 加载987654326@ 带有一条指令(在 SSE 中解压缩 hi/lo,在 Neon 中解压缩)。没有像 AVX2 的 VGATHER 指令这样的帮助,“主”表查找是不可向量化的,但它不必超过四个加载。

【讨论】:

我会尝试一下。我的主要错误是使用非本地内存(m_fResult 和 m_fIndex)。如果我将这些本地化,我会看看会发生什么。

以上是关于SSE/NEON 查表优化的主要内容,如果未能解决你的问题,请参考以下文章

mysql性能优化

if...else代码优化

麻将癞子剪枝算法效率优化

MySQL之优化总结

分块——优化的暴力

mysql优化21条经验(转)