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 的错误。
主要问题:
分别为每个查找加载val
和 val2
。
将 val
和 val2
的索引分别获取到 GPR 中。
将索引向量写入堆栈,然后将它们加载到 GPR 中。
为了让编译器生成更好的代码(每个表格行加载一次),您可能需要像加载双精度一样加载每个表格行,然后将该行转换为两个浮点数的向量并混合行得到齐次向量。在 NEON 和 SSE 上,这应该只需要 4 次加载和 3 或 4 次拆包(比目前的 8 次加载 + 6 次拆包要好得多)。
摆脱多余的堆栈流量可能更难。确保优化器已打开。修复多重加载问题将使堆栈流量减半,因为您只会生成每个索引一次,但要完全摆脱它,可能需要编写程序集而不是内在函数(或使用更新的编译器版本)。
【讨论】:
另外,更改来源。责备编译器进行不可避免的(重新加载),因为你没有明确(足够)你下面的内存无法更改是不公平的...... @FrankH.: (假设表没有标记为 volatile 并且m_fResult
是本地的,几乎可以肯定是这种情况)编译器被允许(并且应该)假设表条目在它下面不会改变。也就是说,是的,源代码可以写得更干净(我在回答中建议了如何做到这一点)。
m_fResult
是这样,但 sftable[]
本身是 NOT。
@FrankH.:只有从sftable
到frac
的负载之间的存储,我们可以看到是本地的,而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[]
中的 交错 布局),因为向量浮点数组 fResult
和 frac
很可能都可以从 @ 加载987654326@ 带有一条指令(在 SSE 中解压缩 hi/lo,在 Neon 中解压缩)。没有像 AVX2 的 VGATHER
指令这样的帮助,“主”表查找是不可向量化的,但它不必超过四个加载。
【讨论】:
我会尝试一下。我的主要错误是使用非本地内存(m_fResult 和 m_fIndex)。如果我将这些本地化,我会看看会发生什么。以上是关于SSE/NEON 查表优化的主要内容,如果未能解决你的问题,请参考以下文章