如何加快浮点到整数转换?

我们在项目中进行了大量的浮点数到整数转换。 基本上,这样的事情

for(int i = 0; i < HUGE_NUMBER; i++) int_array[i] = float_array[i]; 

执行转换的默认C函数非常耗时。

是否有任何工作(可能是手动调整function)可以加快一点点的过程? 我们不太关心精度。

这里的大多数其他答案只是试图消除循环开销。

只有deft_code的答案才能解决真正问题的核心 – 在x86处理器上将浮点转换为整数是非常昂贵的。 deft_code的解决方案是正确的,但他没有给出任何引用或解释。

以下是技巧的来源,一些解释以及特定于是否要向上,向下或向零的版本: 了解您的FPU

很抱歉提供了一个链接,但是这里写的任何内容,除了再现那篇优秀的文章之外,都不会让事情变得清晰。

 inline int float2int( double d ) { union Cast { double d; long l; }; volatile Cast c; cd = d + 6755399441055744.0; return cl; } // this is the same thing but it's // not always optimizer safe inline int float2int( double d ) { d += 6755399441055744.0; return reinterpret_cast(d); } for(int i = 0; i < HUGE_NUMBER; i++) int_array[i] = float2int(float_array[i]); 

双参数不是错误的! 有办法直接用浮子做这个技巧,但是试图覆盖所有角落的情况都很难看。 在当前forms中,如果要截断,则此函数将使浮点数舍入最接近的整数,而使用6755399441055743.5(少0.5)。

我对不同的float-to-int转换方法进行了一些测试 。 简短的回答是假设您的客户具有支持SSE2的CPU并设置/ arch:SSE2编译器标志。 这将允许编译器使用SSE 标量指令,其速度是魔术数字技术的两倍。

否则,如果你有很长的浮动字符串来研磨,请使用SSE2压缩操作。

在SSE3指令集中有一条FISTTP指令可以满足您的需要,但是关于它是否可以被利用并产生比libc更快的结果,我不知道。

时间是否足够大,以至于它超过了启动几个线程的成本?

假设您的盒子上有一个多核处理器或多个处理器,您可以利用这一点,这对于跨多个线程并行化来说是一项微不足道的任务。

关键是要避免_ftol()函数,这是不必要的慢。 对于这样的长数据列表,你最好的选择是使用SSE2指令cvtps2dq将两个打包的浮点数转换为两个打包的int64。 这样做两次(在两个SSE寄存器中获得四个int64)并且可以将它们混合在一起以获得四个int32(丢失每个转换结果的前32位)。 你不需要组装来做这件事; 如果我的内存正确地为我服务,MSVC 会将编译器内在函数暴露给相关指令 – _mm_cvtpd_epi32() 。

如果这样做,浮点数和int数组必须是16字节对齐非常重要,这样SSE2加载/存储内在函数才能以最高效率工作。 另外,我建议您稍微使用软件管道并在每个循环中同时处理16个浮点数,例如(假设这里的“函数”实际上是对编译器内部函数的调用):

 for(int i = 0; i < HUGE_NUMBER; i+=16) { //int_array[i] = float_array[i]; __m128 a = sse_load4(float_array+i+0); __m128 b = sse_load4(float_array+i+4); __m128 c = sse_load4(float_array+i+8); __m128 d = sse_load4(float_array+i+12); a = sse_convert4(a); b = sse_convert4(b); c = sse_convert4(c); d = sse_convert4(d); sse_write4(int_array+i+0, a); sse_write4(int_array+i+4, b); sse_write4(int_array+i+8, c); sse_write4(int_array+i+12, d); } 

这样做的原因是SSE指令具有较长的延迟,因此如果您在xmm0上通过依赖操作立即加载到xmm0中,那么您将有一个停顿。 一次“飞行中”有多个寄存器可以稍微隐藏延迟。 (从理论上讲,一个神奇的无所不知的编译器可能会解决这个问题,但实际上并没有。)

如果没有这个SSE juju,你可以向MSVC提供/ QIfist选项,这将导致它发出单个操作码而不是调用_ftol; 这意味着它将简单地使用在CPU中设置的任何舍入模式,而不确定它是ANSI C的特定截断操作。 微软的文档说/ QIfist已被弃用,因为他们的浮点代码现在很快,但反汇编程序会告诉你这是不合理的乐观。 偶数/ fp:fast简单地导致对_ftol_sse2的调用,这虽然比恶劣的_ftol更快仍然是函数调用,然后是潜在的SSE操作,因此不必要地慢。

顺便说一句,我假设你在x86拱门上 - 如果你在PPC上有相同的VMX操作,或者你可以使用上面提到的魔术数乘法技巧,然后是一个vsel(掩盖了非尾数位)和对齐的存储。

您可以使用一些魔法汇编代码将所有整数加载到处理器的SSE模块中,然后执行等效代码将值设置为整数,然后将它们作为浮点数读取。 我不确定这会更快。 我不是SSE大师,所以我不知道该怎么做。 也许别人可以插话。

在Visual C ++ 2008中,编译器自己生成SSE2调用,如果您使用maxed out优化选项执行发布构建,并查看反汇编(尽管必须满足某些条件,请使用您的代码)。

有关加速整数转换的信息,请参阅此英特尔文章:

http://software.intel.com/en-us/articles/latency-of-floating-point-to-integer-conversions/

根据Microsoft的说法,VS 2005中不推荐使用/ QIfist编译器选项,因为整数转换已经加速。 他们忽略了说它是如何加速的,但是看看反汇编列表可能会给出一个线索。

http://msdn.microsoft.com/en-us/library/z8dh4h17(vs.80).aspx

大多数c编译器为每次float到int转换生成对_ftol的调用。 放置一个简化的浮点一致性开关(如fp:fast)可能会有所帮助 – 如果您理解并接受此开关的其他效果。 除此之外,如果你没事,并且理解不同的舍入行为,那么把东西放在一个紧密的assembly或者内在循环中。 对于像您的示例那样的大循环,您应该编写一个设置浮点控制字一次的函数,然后仅使用fistp指令进行批量舍入,然后重置控制字 – 如果您只使用x86代码路径,但至少你不会改变四舍五入。 读取fld和fistp fpu指令以及fpu控制字。

你用的是什么编译器? 在微软最新的C / C ++编译器中,C / C ++下有一个选项 – >代码生成 – >浮点模型,它有以下选项:快速,精确,严格。 我认为精确是默认值,并且通过在某种程度上模拟FP操作来工作。 如果您使用的是MS编译器,该选项如何设置? 将它设置为“快速”有帮助吗? 在任何情况下,拆卸看起来像什么?

正如上面所说的那样,CPU可以在基本上一条指令中转换float<->int ,并且它没有比那更快(没有SIMD操作)。

另请注意,现代CPU对单个(32位)和双(64位)FP编号使用相同的FP单元,因此除非您尝试节省存储大量浮点数的内存,否则实际上没有理由支持float超过双倍。

在英特尔,您最好的选择是内联SSE2呼叫。

我对你的结果感到惊讶。 你用的是什么编译器? 您是否正在编译优化一路向上? 您是否确认使用valgrind和Kcachegrind这是瓶颈所在? 你在用什么处理器? 汇编代码是什么样的?

转换本身应编译为单个指令。 一个好的优化编译器应该展开循环,以便每个测试和分支完成几次转换。 如果没有发生,您可以手动展开循环

 for(int i = 0; i < HUGE_NUMBER-3; i += 4) { int_array[i] = float_array[i]; int_array[i+1] = float_array[i+1]; int_array[i+2] = float_array[i+2]; int_array[i+3] = float_array[i+3]; } for(; i < HUGE_NUMBER; i++) int_array[i] = float_array[i]; 

如果您的编译器非常可悲,您可能需要使用常见的子表达式来帮助它,例如,

 int *ip = int_array+i; float *fp = float_array+i; ip[0] = fp[0]; ip[1] = fp[1]; ip[2] = fp[2]; ip[3] = fp[3]; 

请回复更多信息!

如果您不太关心舍入语义,可以使用lrint()函数。 这样可以更加自由地进行舍入,并且可以更快。

从技术上讲,它是一个C99函数,但您的编译器可能在C ++中公开它。 一个好的编译器也会将它内联到一条指令(现代G ++将)。

打印文档

四舍五入只有极好的伎俩,只能使用6755399441055743.5(少于0.5)做舍入不起作用。

6755399441055744 = 2 ^ 52 + 2 ^ 51尾数末尾的溢出小数,在fpu寄存器的第51 – 0位中留下您想要的整数。

在IEEE 754中
6755399441055744.0 =

标志指数尾数
0 10000110011 1000000000000000000000000000000000000000000000000000

然而,6755399441055743.5也将编译为0100001100111000000000000000000000000000000000000000000000000000

0.5从最后溢出(向上舍入),这就是为什么它首先起作用的原因。

要做截断,你必须在你的双加0.5,然后这样做,保护数字应该照顾四舍五入到这样做的正确结果。 还要注意64位gcc linux,其中很长一段时间令人讨厌地意味着64位整数。

如果您有非常大的数组(大于几MB – CPU缓存的大小),请为代码计时并查看吞吐量。 你可能正在使内存总线饱和,而不是FP单元。 查看CPU的最大理论带宽,看看它的接近程度。

如果你受到内存总线的限制,额外的线程会让它变得更糟。 您需要更好的硬件(例如更快的内存,不同的CPU,不同的主板)。


回应拉里·格里茨的评论……

你是对的:FPU是一个主要的瓶颈(并且使用xs_CRoundToInt技巧可以让人们非常接近使内存总线饱和)。

以下是Core 2(Q6600)处理器的一些测试结果。 该机器的理论主存储器带宽为3.2 GB / s(L1和L2带宽要高得多)。 该代码是使用Visual Studio 2008编译的。对于32位和64位以及使用/ O2或/ Ox优化的类似结果。

 只写作......
   1866359与33554432数组元素(触及33554432)一起打勾。 带宽:1.91793 GB / s
   154749与262144个数组元素(触及33554432)进行勾选。 带宽:23.1313 GB / s
   108816个带有8192个数组元素的刻度线(触及33554432)。 带宽:32.8954 GB / s

使用铸造......
   5236122与33554432数组元素(触及33554432)一起打勾。 带宽:0.683625 GB / s
   2014309与262144arrays元素(触及33554432)勾选。 带宽:1.77706 GB / s
   1967345与8192个数组元素(触及33554432)进行对比。 带宽:1.81948 GB / s

使用xs_CRoundToInt ...
   1490583与33554432数组元素(触及33554432)勾选。 带宽:2.40144 GB / s
   1079530与262144个数组元素(触及33554432)对齐。 带宽:3.31584 GB / s
   1008407与8192个数组元素(触及33554432)对齐。 带宽:3.5497 GB / s 

(Windows)源代码:

 // floatToIntTime.cpp : Defines the entry point for the console application. // #include  #include  using namespace std; double const _xs_doublemagic = double(6755399441055744.0); inline int xs_CRoundToInt(double val, double dmr=_xs_doublemagic) { val = val + dmr; return ((int*)&val)[0]; } static size_t const N = 256*1024*1024/sizeof(double); int I[N]; double F[N]; static size_t const L1CACHE = 128*1024/sizeof(double); static size_t const L2CACHE = 4*1024*1024/sizeof(double); static size_t const Sz[] = {N, L2CACHE/2, L1CACHE/2}; static size_t const NIter[] = {1, N/(L2CACHE/2), N/(L1CACHE/2)}; int main(int argc, char *argv[]) { __int64 freq; QueryPerformanceFrequency((LARGE_INTEGER*)&freq); cout << "WRITING ONLY..." << endl; for (int t=0; t<3; t++) { __int64 t0,t1; QueryPerformanceCounter((LARGE_INTEGER*)&t0); size_t const niter = NIter[t]; size_t const sz = Sz[t]; for (size_t i=0; i