为什么floor()这么慢?

时间:2009-05-05 09:42:32

标签: c performance visual-c++ x86 intel

我最近写了一些代码(ISO / ANSI C),并对它所取得的糟糕表现感到惊讶。长话短说,事实证明罪魁祸首是floor()功能。它不仅速度慢,而且没有矢量化(使用英特尔编译器,也就是ICL)。

以下是为2D矩阵中的所有单元格执行底板的一些基准:

VC:  0.10
ICL: 0.20

将其与简单演员比较:

VC:  0.04
ICL: 0.04

floor()怎么能比简单演员慢得多?!它基本上是相同的(负数除外)。 第二个问题:有人知道超快floor()实施吗?

PS:这是我进行基准测试的循环:

void Floor(float *matA, int *intA, const int height, const int width, const int width_aligned)
{
    float *rowA=NULL;
    int   *intRowA=NULL;
    int   row, col;

    for(row=0 ; row<height ; ++row){
        rowA = matA + row*width_aligned;
        intRowA = intA + row*width_aligned;
#pragma ivdep
        for(col=0 ; col<width; ++col){
            /*intRowA[col] = floor(rowA[col]);*/
            intRowA[col] = (int)(rowA[col]);
        }
    }
}

8 个答案:

答案 0 :(得分:41)

有些事情比投射更慢,并阻止矢量化。

最重要的一个:

楼层可以修改全局状态。如果传递的值太大而无法以float格式表示为整数,则 errno 变量将设置为 EDOM 。还完成了对NaN的特殊处理。所有这些行为都适用于想要检测溢出情况并以某种方式处理情况的应用程序(不要问我怎么做)。

检测这些有问题的条件并不简单,占地面执行时间的90%以上。实际的舍入很便宜,可以内联/矢量化。这也是很多代码,所以内联整个函数会使你的程序运行得更慢。

某些编译器具有特殊的编译器标志,允许编译器优化一些很少使用的c标准规则。例如, GCC 可以被告知你根本不对errno感兴趣。为此,请传递 -fno-math-errno -ffast-math 。 ICC和VC可能有类似的编译器标志。

顺便说一句 - 您可以使用简单的演员表来滚动自己的地板功能。你只需要以不同的方式处理消极和积极的情况。如果您不需要特殊处理溢出和NaN,那可能会快得多。

答案 1 :(得分:17)

如果要将floor()操作的结果转换为int,并且如果您不担心溢出,则以下代码比(int)floor(x)快得多:

inline int int_floor(double x)
{
  int i = (int)x; /* truncate */
  return i - ( i > x ); /* convert trunc to floor */
}

答案 2 :(得分:10)

无枝地板和天花板(更好地利用pipiline)无错误检查

int f(double x)
{
    return (int) x - (x < (int) x); // as dgobbi above, needs less than for floor
}

int c(double x)
{
    return (int) x + (x > (int) x);
}

或使用楼层

int c(double x)
{
    return -(f(-x));
}

答案 3 :(得分:4)

在现代x86 CPU上,大型阵列的实际最快实现是

  • 将MXCSR FP舍入模式更改为向-Infinity(aka floor)舍入。在C语言中,fenv的东西或_mm_getcsr / _mm_setcsr应该是可能的。
  • 对SIMD向量执行_mm_cvtps_epi32遍历数组,并使用当前的舍入模式将4 float转换为32位整数。 (并将结果向量存储到目标位置。)

    cvtps2dq xmm0, [rdi]是自K10或Core 2以来在任何Intel或AMD CPU上的单个微融合uop。(https://agner.org/optimize/)与256位AVX版本相同,带有YMM向量。

  • 使用MXCSR的原始值将当前舍入模式恢复为普通的IEEE默认模式。 (四舍五入,甚至抢七)

这允许每个时钟周期加载+转换+存储1个SIMD向量结果,就像截断一样快。 (SSE2有一个特殊的FP-> int转换指令用于截断,正是因为C编译器非常需要它。在x87的糟糕年代,甚至(int)x都需要将x87舍入模式更改为截断然后返回。 cvttps2dq for packed float->int with truncation(请注意助记符中多余的t)或标量,从XMM到整数寄存器,从cvttss2sicvttsd2si到标量double到标量整数。

通过一些循环展开和/或良好的优化,这应该是可能的,而不会在前端造成瓶颈,假设没有缓存丢失瓶颈,则只有每时钟1个存储吞吐量。 (在Skylake之前的Intel上,也瓶颈于每时钟1个压缩转换吞吐量。)即,使用SSE2,AVX或AVX512,每个周期 16、32或64个字节。


在不更改当前舍入模式的情况下,您需要SSE4.1 roundps使用选择的舍入模式将float舍入到最接近的整数float。或者,您也可以使用其他答案中显示的技巧之一,这些技巧适用于大小足够小以适合有符号的32位整数的浮点数,因为这毕竟是您的最终目标格式。)


(通过使用正确的编译器选项(如-fno-math-errno和正确的-march-msse4选项,编译器可以使用floor内联roundps或标量和/或双精度等价物,例如roundsd xmm1, xmm0, 1,但这花费2 oups,并且在Haswell上标量或向量的时钟频率为每2个时钟1个。实际上,gcc8.2将内联roundsd到{{ 1}},即使没有任何快速选择,as you can see on the Godbolt compiler explorer。但这与floor一起使用。很遗憾,它不是x86-64的基准,因此,如果您的计算机支持,则需要启用它。)

答案 4 :(得分:1)

是的,floor()在所有平台上都非常慢,因为它必须实现IEEE fp规范中的许多行为。你不能在内循环中真正使用它。

我有时使用宏来估算floor():

#define PSEUDO_FLOOR( V ) ((V) >= 0 ? (int)(V) : (int)((V) - 1))

它的行为与floor()不完全相同:例如,floor(-1) == -1PSEUDO_FLOOR(-1) == -2,但它足够接近大多数用途。

答案 5 :(得分:1)

实际上是无分支版本,需要在浮点域和整数域之间进行一次转换,会将值x移至所有正或负范围,然后进行强制转换/截断并将其移回。

long fast_floor(double x)
{
    const unsigned long offset = ~(ULONG_MAX >> 1);
    return (long)((unsigned long)(x + offset) - offset);
}

long fast_ceil(double x) {
    const unsigned long offset = ~(ULONG_MAX >> 1);
    return (long)((unsigned long)(x - offset) + offset );
}

如评论中所指出的,此实现依赖于临时值x +- offset不溢出。

在64位平台上,使用int64_t中间值的原始代码将导致三个指令内核,对于int32_t缩小范围下限/上限,它们也可用,其中|x| < 0x40000000-

inline int floor_x64(double x) {
   return (int)((int64_t)(x + 0x80000000UL) - 0x80000000LL);
}
inline int floor_x86_reduced_range(double x) {
   return (int)(x + 0x40000000) - 0x40000000;
}

答案 6 :(得分:-1)

  1. 他们不做同样的事情。 floor()是一个函数。因此,使用它会产生函数调用,分配堆栈帧,复制参数和检索结果。 转换不是函数调用,因此它使用更快的机制(我相信它可能使用寄存器来处理值)。
  2. 可能floor()已经过优化。
  3. 您可以从算法中获得更多性能吗?也许切换行和列可能会有所帮助?你可以缓存常见值吗?您的所有编译器都进行了优化吗?你可以切换操作系统吗?编译器? Jon Bentley's Programming Pearls对可能的优化进行了很好的审核。

答案 7 :(得分:-3)

快速双轮

double round(double x)
{
    return double((x>=0.5)?(int(x)+1):int(x));
}

终端日志

测试custom_1 8.3837

测试native_1 18.4989

测试custom_2 8.36333

测试native_2 18.5001

测试custom_3 8.37316

测试native_3 18.5012


<强>测试

void test(char* name, double (*f)(double))
{
    int it = std::numeric_limits<int>::max();

    clock_t begin = clock();

    for(int i=0; i<it; i++)
    {
        f(double(i)/1000.0);
    }
    clock_t end = clock();

    cout << "test " << name << " " << double(end - begin) / CLOCKS_PER_SEC << endl;

}

int main(int argc, char **argv)
{

    test("custom_1",round);
    test("native_1",std::round);
    test("custom_2",round);
    test("native_2",std::round);
    test("custom_3",round);
    test("native_3",std::round);
    return 0;
}

<强>结果

类型转换和使用你的大脑比使用本机功能快3倍。