繁体   English   中英

为什么floor()这么慢?

[英]Why is floor() so slow?

我最近写了一些代码(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]);
        }
    }
}

一些事情比投射更慢,防止矢量化。

最重要的一个:

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

检测这些有问题的条件并不简单,占地面执行时间的90%以上。 实际的舍入很便宜,可以内联/矢量化。 此外,它还有很多代码,因此内联整个函数会使您的程序运行速度变慢。

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

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

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

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

无枝地板和天花板(更好地利用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));
}

现代x86 CPU上大型阵列的实际最快实现方式是

  • 将MXCSR FP舍入模式更改为朝向-Infinity(也称为floor )舍入 在C中,这应该可以使用fenv stuff或_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/ )与具有YMM向量的256位AVX版本相同。

  • 使用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之前的英特尔,也是每时钟1个打包转换吞吐量的瓶颈。)即每个周期16,32或64字节,使用SSE2,AVX或AVX512。


在不改变当前的舍入模式,则需要SSE4.1 roundps于圆形float到最接近的整数float使用舍入模式的选择。 或者你可以在其他答案中使用其中一个技巧,这些答案适用于具有足够小幅度的浮点数,以适应有符号的32位整数,因为这无论如何都是你的终极目标格式。)


(使用正确的编译器选项,如-fno-math-errno和right -march-msse4选项,编译器可以使用roundps或标量和/或双精度等效内联floor ,例如roundsd xmm1, xmm0, 1但是,对于标量或向量,这需要2 uops并且Haswell每2时钟吞吐量为1。实际上,即使没有任何快速数学选项,gcc8.2也会内联roundsd floor正如你在Godbolt编译器浏览器中看到的那样 。但是-march=haswell 。遗憾的是,它不是x86-64的基线,因此如果你的机器支持它,你需要启用它。)

需要在浮点和整数域之间进行单次转换的实际无分支版本会将值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减少范围floor / ceil,其中|x| < 0x40000000 |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;
}

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

我有时使用宏来近似floor():

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

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

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

快速双轮

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

test 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倍。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM