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

时间:2009-01-09 20:38:20

标签: c++ c performance optimization floating-point

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

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

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

是否有任何解决方法(可能是手动调整功能)可以加快这一过程的速度?我们不太关心精度。

16 个答案:

答案 0 :(得分:15)

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

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

这是技巧的来源,有一些解释以及特定于是否要向上,向下或向零舍入的版本:Know your FPU

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

答案 1 :(得分:14)

inline int float2int( double d )
{
   union Cast
   {
      double d;
      long l;
    };
   volatile Cast c;
   c.d = d + 6755399441055744.0;
   return c.l;
}

// this is the same thing but it's
// not always optimizer safe
inline int float2int( double d )
{
   d += 6755399441055744.0;
   return reinterpret_cast<int&>(d);
}

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

双参数不是错!有办法直接用浮子做这个技巧,但是试图覆盖所有角落的情况变得丑陋。在当前形式中,如果你想要截断,这个函数会将浮点数舍入最接近的整数,而是使用6755399441055743.5(少0.5)。

答案 2 :(得分:8)

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

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

答案 3 :(得分:3)

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

答案 4 :(得分:2)

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

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

答案 5 :(得分:2)

关键是要避免_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 arch - 如果你在PPC上有相同的VMX操作,或者你可以使用上面提到的魔术数乘法技巧,然后使用vsel(到掩盖非尾数位)和对齐的存储。

答案 6 :(得分:1)

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

答案 7 :(得分:1)

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

答案 8 :(得分:1)

请参阅此英特尔文章,了解如何加快整数转换:

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

答案 9 :(得分:1)

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

答案 10 :(得分:0)

您使用的是什么编译器?在微软最新的C / C ++编译器中,C / C ++下有一个选项 - &gt;代码生成 - &gt;浮点模型,有选项:快速,精确,严格。我认为精确是默认值,并且通过在某种程度上模拟FP操作来工作。如果您使用的是MS编译器,该选项如何设置?是否有助于将其设置为“快速”?无论如何,反汇编是什么样的?

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

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

答案 11 :(得分:0)

On Intel你最好的选择是内联SSE2电话。

答案 12 :(得分:0)

我对你的结果感到惊讶。你用的是什么编译器?您是否在优化过程中进行编译?你确认使用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];

报告更多信息!

答案 13 :(得分:0)

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

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

lrint documentation

答案 14 :(得分:0)

仅限四舍五入 优秀的技巧,只有使用6755399441055743.5(少于0.5)做舍入将无法正常工作。

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

在IEEE 754中 6755399441055744.0 =

签署指数尾数
 0 10000110011 100000000000000000000000000000000000000000000000000000

6755399441055743.5 然而,也将编译为 0100001100111000000000000000000000000000000000000000000000000000

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

要进行截断,你必须在你的double中添加0.5,然后执行此操作 保护数字应该通过这种方式处理四舍五入到正确的结果。 还要注意64位gcc linux,其中很长一段时间令人讨厌地意味着64位整数。

答案 15 :(得分:-1)

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

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

<小时/> 回应拉里·格里茨的评论......

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

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

WRITING ONLY...
  1866359 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 1.91793 GB/s
  154749 ticks with 262144 array elements (33554432 touched).  Bandwidth: 23.1313 GB/s
  108816 ticks with 8192 array elements (33554432 touched).  Bandwidth: 32.8954 GB/s

USING CASTING...
  5236122 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 0.683625 GB/s
  2014309 ticks with 262144 array elements (33554432 touched).  Bandwidth: 1.77706 GB/s
  1967345 ticks with 8192 array elements (33554432 touched).  Bandwidth: 1.81948 GB/s

USING xs_CRoundToInt...
  1490583 ticks with 33554432 array elements (33554432 touched).  Bandwidth: 2.40144 GB/s
  1079530 ticks with 262144 array elements (33554432 touched).  Bandwidth: 3.31584 GB/s
  1008407 ticks with 8192 array elements (33554432 touched).  Bandwidth: 3.5497 GB/s

(Windows)源代码:

// floatToIntTime.cpp : Defines the entry point for the console application.
//

#include <windows.h>
#include <iostream>

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<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = 13;
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  cout << "USING CASTING..." << 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<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = (int)F[n];
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  cout << "USING xs_CRoundToInt..." << 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<niter; i++) {
      for (size_t n=0; n<sz; n++) {
        I[n] = xs_CRoundToInt(F[n]);
      }
    }
    QueryPerformanceCounter((LARGE_INTEGER*)&t1);
    double bandwidth = 8*niter*sz / (((double)(t1-t0))/freq) / 1024/1024/1024;
    cout << "  " << (t1-t0) << " ticks with " << sz 
         << " array elements (" << niter*sz << " touched).  " 
         << "Bandwidth: " << bandwidth << " GB/s" << endl;
  }

  return 0;
}