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

时间:2015-07-22 23:40:04

标签: c performance integer sse division

根据this question的答案/评论,我用 gcc 4.9.2(MinGW64)编写了一个性能测试来估算多个整数除法的哪种方式更快,如下所示:< / p>

#include <emmintrin.h>  // 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测试变得更快,尽管昂贵的转换为&amp;从浮点。此外,我想知道有多少结果取决于编译器和架构。

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

1 个答案:

答案 0 :(得分:2)

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

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

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

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

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

代码中的其他内容:

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

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

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

最新Intel CPU的性能

在最近的英特尔设计中,

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

仅仅对于除法,单个整数除法比向量FP除法慢。即使转换为/来自float,还是unpack / pack,你也会提前退出。

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

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