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

Posted

技术标签:

【中文标题】用非常数除数进行矢量化整数除法的最快方法【英文标题】:Fastest method of vectorized integer division by non-constant divisor 【发布时间】:2015-07-22 23:40:03 【问题描述】:

基于this question 的答案/cmets,我用 gcc 4.9.2 (MinGW64) 编写了一个性能测试,以估计哪种方式的多个整数除法更快,如下所示:

#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 测试却变得更快,尽管昂贵的浮点转换成本。此外,我想知道有多少结果取决于编译器和架构。

总结一下:到底哪个更快:一分一分还是浮点绕道?

【问题讨论】:

似乎unsigned short x[8] 在第一次测试中被修改了。所以第二个测试从不同的值开始。 (而且所有x 的值似乎都很快转到0。) 操作持续时间不应受参数影响,编译器不应检测x何时为0。当我设置本地红利时没有区别,所以x 没关系。 你可以做得更好:你可以乘以被除数的模逆,而不是除法。这是编译器将通过编译时常量进行除法的操作,您也可以在运行时执行此操作,但需要做一些工作。 @Youka "操作持续时间不应该受参数影响" 我会说他们会这样做,至少在一般情况下是这样。例如。英特尔的一位指南说:““DIV/IDIV r64”的吞吐量随输入 RDX:RAX 中有效数字的数量而变化”。不过,我并不是说这对你来说很重要。 【参考方案1】:

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

回复:您的代码: 当给它一个像这样的微不足道的循环时,你必须小心检查编译器实际产生的内容。例如它实际上是每次迭代都加载/存储回RAM吗?还是将变量保存在寄存器中,只在最后存储?

您的基准测试偏向于整数除法循环,因为向量除法器并未在向量循环中保持 100% 占用,但整数除法器在 int 循环中保持 100% 占用。 (这些段落是在 cmets 讨论之后添加的。之前的答案没有解释太多关于保持分隔符和依赖链的内容。)

您的向量循环中只有一个依赖链,因此向量除法器在产生第二个结果后每次迭代都会空闲几个周期,而 convert fp->si、pack、unpack、convert si-> 的链fp 发生。您已经进行了设置,因此您的吞吐量受限于整个循环携带的依赖链的长度,而不是 FP 分隔符的吞吐量。如果每次迭代的数据是独立的(或者至少有几个独立的值,比如 int 循环有 8 个数组元素),那么一组值的解包/转换和转换/打包将与 @987654324 重叠@ 另一个向量的执行时间。矢量除法器仅部分流水线化,其他一切都流水线化。

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

代码中的其他内容:

您在内部循环中有__m128 xmm1 = _mm_set1_ps(i);_set1 的 arg 不是编译时常量,通常至少有 2 条指令:movdpshufd。在这种情况下,也是一个整数到浮点数的转换。保持循环计数器的浮点向量版本,通过添加1.0 的向量来增加,会更好。 (尽管这可能不会进一步影响您的速度测试,因为这种多余的计算可能会与其他东西重叠。)

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

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

最新 Intel CPU 的性能

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

因此,仅对于除法而言,单个整数除法比向量 FP 除法要慢。即使转换为浮点数/从浮点数转换,以及解包/打包,您也会领先一步。

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

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

【讨论】:

_mm_cvtepi16_epi32 需要 SSE4.1 但我也想支持旧处理器,所以没有选择。就我而言,我无法以 FP 格式保存数据,因为我将它们作为像素 (unsigned char) 获取并在模块化函数中处理它们。乘法和移位的技巧适用于常量,但我的值是可变的。您的第二句话非常有用,因为确实 -O3 x 只转换为 FP 一次并一直保留在 xmm 寄存器中直到结束,循环计数器也变为浮点数 - 昂贵的重复转换消失了。 看来我需要一个更好的测试代码来说明性能结果。我认为-O0 给出了一个很好的一般提示。但是考虑到-O3 div_sse 只是比 div_x86 快一点,而且没有所有这些转换...... div_sse 仍然从 int 转换为 float 并返回,不是吗?我可以看到编译器跳过循环中的存储/加载,但不是转换。另请注意,您正在循环中测试部门,这是所有可能发生的事情。在实践中,还会有其他的东西可以和 div 的依赖链重叠,对吧? 我用谷歌搜索并发现向量/标量的整数除法,即使标量不是编译时常数。我没有检查细节,即它是否是近似值。我更新了我的答案(新的第一段。) 我想到了一个更好的方法来解释我想说的话:如果你需要划分许多独立的值(超过 8 个),那么向量绝对是要走的路。所有的解包/转换/重新转换/打包指令都是完全流水线的(每个周期一个结果)。他们应该能够保持非流水线 FP 分频器被占用,如果他们不必等待前一个 FP 分频器的输出才能执行下一个 convert/pack/unpack/convert 循环。这是吞吐量和延迟之间的差异。 FP 除法器产生 4 个结果的速度比整数除法器产生 1 的速度快。

以上是关于用非常数除数进行矢量化整数除法的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

剑指offer-001-整数的除法

C++的大数除法最快速度的算法

Leetcode练习(Python):数学类:第29题:两数相除:给定两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用乘法除法和 mo(

1009:带余除法

带余除法

用辗转相除法求两个整数的最大公约数