用非常数除数进行矢量化整数除法的最快方法
Fastest method of vectorized integer division by non-constant divisor
基于 this question 的 answers/comments 我用 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 测试却通过昂贵的浮点转换变得更快。此外,我想知道有多少结果取决于编译器和架构。
总结一下:最后哪个更快:逐一除法还是浮点绕道?
将一个向量的所有元素除以同一个标量可以通过整数乘法和移位来完成。 libdivide (C/C++, zlib license) provides some inline functions to do this for scalars (e.g. int
), and for dividing vectors by scalars. Also see SSE integer division?(正如您在问题中提到的那样)用于提供近似结果的类似技术。如果将相同的标量应用于大量向量,则效率会更高。 libdivide 没有说结果不准确,但我没有调查过。
回复:您的代码:
当给它一个像这样的简单循环时,你必须小心检查编译器实际产生的内容。例如它实际上是 loading/storing 每次迭代都返回到 RAM 吗?还是将变量保存在寄存器中,只存储在最后?
您的基准测试偏向整数除法循环,因为向量除法器在向量循环中未保持 100% 占用,但整数除法器在 int 循环中保持 100% 占用。 (这些段落是在评论讨论后添加的。之前的答案没有解释太多关于保持分隔线和依赖链的内容。)
你的向量循环中只有一个依赖链,所以向量除法器在产生第二个结果后每次迭代都会闲置几个循环,而转换链 fp->si, pack, unpack, convert si ->fp 发生。您已进行设置,因此您的吞吐量受限于整个循环携带的依赖链的长度,而不是 FP 分隔符的吞吐量。如果每次迭代的数据都是独立的(或者至少有几个独立的值,比如 int 循环有 8 个数组元素),那么一组值的 unpack/convert 和 convert/pack 会重叠另一个向量的执行时间为 divps
。矢量除法器仅部分流水线化,但其他所有内容都完全流水线化。
这是吞吐量和延迟之间的差异,以及为什么它对流水线乱序执行很重要 CPU。
代码中的其他内容:
你在内部循环中有 __m128 xmm1 = _mm_set1_ps(i);
。 _set1
带有不是编译时常量的 arg 通常至少有 2 条指令:movd
和 pshufd
。在这种情况下,还有 int 到 float 的转换。保留循环计数器的浮点矢量版本(通过添加 1.0
的矢量来递增)会更好。 (虽然这可能不会进一步影响您的速度测试,因为这种多余的计算可能会与其他内容重叠。)
用零解包工作正常。 SSE4.1 __m128i _mm_cvtepi16_epi32 (__m128i a)
是另一种方式。 pmovsxwd
速度相同,但不需要归零寄存器。
如果您打算将除法转换为 FP,您是否考虑过将数据保留为 FP 一段时间?取决于您的算法,您需要如何进行舍入。
近期 Intel 的性能 CPUs
在最近的英特尔设计中,divps
(打包单浮点数)是 10-13 个周期延迟,每 7 个周期一个吞吐量。 div / idiv r16
(GP reg 中的(无符号)整数除法)是 23-26 个周期延迟,每 9 或 8 个周期吞吐量一个。 div
是 11 微指令,所以它甚至会在它通过管道的某些时候妨碍其他事情的发布/执行。 (divps
是一个 uop。)因此,Intel CPU 并不是真正设计用于快速整数除法,而是为 FP 除法做出努力。
因此仅就除法而言,单个整数除法比向量 FP 除法慢。即使使用转换 to/from 浮点数和 unpack/pack.
,你也会领先
如果您可以在向量 regs 中执行其他整数操作,那就太理想了。否则,您必须将整数输入/输出向量 regs。如果整数在 RAM 中,向量加载就可以了。如果您一次生成一个,PINSRW
是一个选项,但仅存储到内存以设置矢量加载可能是加载完整矢量的更快方法。类似于使用 PEXTRW
或通过存储到 RAM 来取回数据。如果您想要 GP 寄存器中的值,请在转换回 int 后跳过 pack
,而只需从您的值所在的两个向量寄存器中的任何一个中选择 MOVD / PEXTRD
。insert/extract 指令需要两个 uops在 Intel 上,这意味着它们占用两个 "slots",而大多数指令只占用一个融合域 uop。
你的计时结果显示标量代码没有随着编译器优化而改进,这是因为 CPU 可以重叠其他元素的冗长的非优化 load/store 指令,而除法单元是瓶颈。另一方面,矢量循环只有一个或两个依赖链,每次迭代都依赖于前一次,因此增加延迟的额外指令不能与任何东西重叠。使用 -O0
进行测试几乎没有用。
基于 this question 的 answers/comments 我用 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 测试却通过昂贵的浮点转换变得更快。此外,我想知道有多少结果取决于编译器和架构。
总结一下:最后哪个更快:逐一除法还是浮点绕道?
将一个向量的所有元素除以同一个标量可以通过整数乘法和移位来完成。 libdivide (C/C++, zlib license) provides some inline functions to do this for scalars (e.g. int
), and for dividing vectors by scalars. Also see SSE integer division?(正如您在问题中提到的那样)用于提供近似结果的类似技术。如果将相同的标量应用于大量向量,则效率会更高。 libdivide 没有说结果不准确,但我没有调查过。
回复:您的代码: 当给它一个像这样的简单循环时,你必须小心检查编译器实际产生的内容。例如它实际上是 loading/storing 每次迭代都返回到 RAM 吗?还是将变量保存在寄存器中,只存储在最后?
您的基准测试偏向整数除法循环,因为向量除法器在向量循环中未保持 100% 占用,但整数除法器在 int 循环中保持 100% 占用。 (这些段落是在评论讨论后添加的。之前的答案没有解释太多关于保持分隔线和依赖链的内容。)
你的向量循环中只有一个依赖链,所以向量除法器在产生第二个结果后每次迭代都会闲置几个循环,而转换链 fp->si, pack, unpack, convert si ->fp 发生。您已进行设置,因此您的吞吐量受限于整个循环携带的依赖链的长度,而不是 FP 分隔符的吞吐量。如果每次迭代的数据都是独立的(或者至少有几个独立的值,比如 int 循环有 8 个数组元素),那么一组值的 unpack/convert 和 convert/pack 会重叠另一个向量的执行时间为 divps
。矢量除法器仅部分流水线化,但其他所有内容都完全流水线化。
这是吞吐量和延迟之间的差异,以及为什么它对流水线乱序执行很重要 CPU。
代码中的其他内容:
你在内部循环中有 __m128 xmm1 = _mm_set1_ps(i);
。 _set1
带有不是编译时常量的 arg 通常至少有 2 条指令:movd
和 pshufd
。在这种情况下,还有 int 到 float 的转换。保留循环计数器的浮点矢量版本(通过添加 1.0
的矢量来递增)会更好。 (虽然这可能不会进一步影响您的速度测试,因为这种多余的计算可能会与其他内容重叠。)
用零解包工作正常。 SSE4.1 __m128i _mm_cvtepi16_epi32 (__m128i a)
是另一种方式。 pmovsxwd
速度相同,但不需要归零寄存器。
如果您打算将除法转换为 FP,您是否考虑过将数据保留为 FP 一段时间?取决于您的算法,您需要如何进行舍入。
近期 Intel 的性能 CPUs
在最近的英特尔设计中,divps
(打包单浮点数)是 10-13 个周期延迟,每 7 个周期一个吞吐量。 div / idiv r16
(GP reg 中的(无符号)整数除法)是 23-26 个周期延迟,每 9 或 8 个周期吞吐量一个。 div
是 11 微指令,所以它甚至会在它通过管道的某些时候妨碍其他事情的发布/执行。 (divps
是一个 uop。)因此,Intel CPU 并不是真正设计用于快速整数除法,而是为 FP 除法做出努力。
因此仅就除法而言,单个整数除法比向量 FP 除法慢。即使使用转换 to/from 浮点数和 unpack/pack.
,你也会领先如果您可以在向量 regs 中执行其他整数操作,那就太理想了。否则,您必须将整数输入/输出向量 regs。如果整数在 RAM 中,向量加载就可以了。如果您一次生成一个,PINSRW
是一个选项,但仅存储到内存以设置矢量加载可能是加载完整矢量的更快方法。类似于使用 PEXTRW
或通过存储到 RAM 来取回数据。如果您想要 GP 寄存器中的值,请在转换回 int 后跳过 pack
,而只需从您的值所在的两个向量寄存器中的任何一个中选择 MOVD / PEXTRD
。insert/extract 指令需要两个 uops在 Intel 上,这意味着它们占用两个 "slots",而大多数指令只占用一个融合域 uop。
你的计时结果显示标量代码没有随着编译器优化而改进,这是因为 CPU 可以重叠其他元素的冗长的非优化 load/store 指令,而除法单元是瓶颈。另一方面,矢量循环只有一个或两个依赖链,每次迭代都依赖于前一次,因此增加延迟的额外指令不能与任何东西重叠。使用 -O0
进行测试几乎没有用。