通过非常数除数进行矢量化整数除法的最快方法

基于这个问题的答案/评论,我用gcc 4.9.2(MinGW64)编写了一个性能测试来估计多个整数除法的哪种方式更快,如下:

#include  // SSE2 static unsigned short x[8] = {0, 55, 2, 62003, 786, 5555, 123, 32111}; // Dividend __attribute__((noinline)) static void test_div_x86(unsigned i){ for(; i; --i) x[0] /= i, x[1] /= i, x[2] /= i, x[3] /= i, x[4] /= i, x[5] /= i, x[6] /= i, x[7] /= i; } __attribute__((noinline)) static void test_div_sse(unsigned i){ for(; i; --i){ __m128i xmm0 = _mm_loadu_si128((const __m128i*)x); __m128 xmm1 = _mm_set1_ps(i); _mm_storeu_si128( (__m128i*)x, _mm_packs_epi32( _mm_cvtps_epi32( _mm_div_ps( _mm_cvtepi32_ps(_mm_unpacklo_epi16(xmm0, _mm_setzero_si128())), xmm1 ) ), _mm_cvtps_epi32( _mm_div_ps( _mm_cvtepi32_ps(_mm_unpackhi_epi16(xmm0, _mm_setzero_si128())), xmm1 ) ) ) ); } } int main(){ const unsigned runs = 40000000; // Choose a big number, so the compiler doesn't dare to unroll loops and optimize with constants test_div_x86(runs), test_div_sse(runs); return 0; } 

GNU Gprof和工具参数的结果。

 /* gcc -O? -msse2 -pg -o test.o -c test.c g++ -o test test.o -pg test gprof test.exe gmon.out ----------------------------------- test_div_sse(unsigned int) test_div_x86(unsigned int) -O0 2.26s 1.10s -O1 1.41s 1.07s -O2 0.95s 1.09s -O3 0.77s 1.07s */ 

现在我很困惑为什么x86测试几乎没有得到优化,并且SSE测试变得更快,尽管昂贵的转换为浮点数。 此外,我想知道有多少结果取决于编译器和架构。

总结一下:最后什么更快:一个接一个地划分还是浮点绕行?

用相同的标量划分矢量的所有元素可以用整数乘法和移位来完成。 libdivide (C / C ++,zlib许可证)提供了一些内联函数来为标量(例如int )执行此操作,以及通过标量分割向量。 另见SSE整数除法? (正如你在你的问题中提到的)类似的技术给出了近似的结果。 如果将相同的标量应用于大量向量,则效率更高。 libdivide没有说结果不准确,但我没有调查过。

re:你的代码:当给它一个像这样的简单循环时,你必须要小心检查编译器实际生成的内容。 例如,它是否每次迭代都实际加载/存储回RAM? 或者它是否将变量保存在寄存器中,并且只存储在最后?

您的基准偏向于整数除法循环,因为向量分频器在向量循环中不会保持100%占用,但整数分频器在int循环中保持100%占用。 (这些段落是在评论中讨论后添加的。之前的答案没有解释为什么要保持分配器和依赖链。)

在向量循环中只有一个依赖链,因此向量分频器在产生第二个结果后每次迭代都空闲几个周期,而转换链fp-> si,pack,unpack,convert si-> fp发生。 您已经进行了设置,因此您的吞吐量受到整个循环传输依赖关系链的长度的限制,而不是FP分频器的吞吐量。 如果每次迭代的数据是独立的(或者至少有几个独立的值,比如你有如何为int循环提供8个数组元素),那么一组值的解包/转换和转换/包将与divps执行重叠另一个向量的时间。 向量分隔符仅部分流水线化,但如果完全流水线化则是其他所有内容。

这是吞吐量和延迟之间的差异,以及为什么它对于流水线无序执行CPU很重要。

代码中的其他内容:

你有__m128 xmm1 = _mm_set1_ps(i); 在内循环中。 带有不是编译时常量的arg的_set1通常至少有2条指令: pshufdpshufd 。 在这种情况下,也是一个int-to-float转换。 保持循环计数器的浮点矢量版本,通过添加1.0的向量来增加,会更好。 (虽然这可能不会进一步甩掉你的速度测试,因为这个多余的计算可能会与其他东西重叠。)

零件拆包工作正常。 SSE4.1 __m128i _mm_cvtepi16_epi32 (__m128i a)是另一种方式。 pmovsxwd速度相同,但不需要归零寄存器。

如果您要转换为FP进行分割,您是否考虑过将数据保留为FP一段时间? 取决于您的算法如何进行四舍五入。

近期英特尔CPU的性能

在最近的英特尔设计中, divps (打包单浮点数)的周期延迟为10-13,吞吐量为每7个周期一个。 div / idiv r16 (GP reg中的(无符号)整数除法)是23-26周期延迟,每9或8个周期吞吐量一个。 div是11 uops,所以它甚至会妨碍在其通过管道的某些时间发出/执行的其他事情。 ( divps是单个divps 。)因此,Intel CPU并不是真正设计为快速整数除法,而是为FP划分做出努力。

因此,仅对于除法,单个整数除法比向量FP除法慢。 即使转换到/从浮动和unpack / pack,你也会提前出来。

如果你可以在向量regs中执行其他整数运算,那将是理想的。 否则你必须将整数输入/输出向量regs。 如果int在RAM中,则向量加载很好。 如果你一次只生成一个, PINSRW是一个选项,但是存储到内存以设置矢量加载可能是加载完整矢量的更快方法。 类似于使用PEXTRW或通过存储到RAM来恢复数据。 如果你想要GP寄存器中的值,在转换回int之后跳过pack ,只需要从你的值所在的两个向量寄存器中的MOVD / PEXTRD中插入。插入/提取指令在Intel上占用两个uop,这意味着它们占用了两个“槽”,相比大多数指令只采用一个融合域uop。

您的计时结果显示标量代码没有通过编译器优化得到改善,因为CPU可以重叠其他元素的详细非优化加载/存储指令,而除法单元是瓶颈。 另一方面,向量循环只有一个或两个依赖链,每次迭代都依赖于前一个,因此添加延迟的额外指令不能与任何东西重叠。 使用-O0测试几乎-O0