避免对楼层的呼叫()

时间:2010-02-28 19:24:29

标签: c++ optimization graphics 3d floor

我正在编写一段代码,我需要处理不一定在0到1范围内的uvs(2D纹理坐标)。举个例子,有时我会得到一个u的组件为1.2。为了处理这个问题,我实现了一个包装,它通过执行以下操作来导致平铺:

u -= floor(u)
v -= floor(v)

这样做会使1.2变为0.2,这是所希望的结果。它还处理负面情况,例如-0.4变为0.6。

然而,这些对地板的调用相当缓慢。我使用英特尔VTune描述了我的应用程序,我正在花费大量的周期来进行这种操作。

在对这个问题做了一些背景阅读后,我提出了以下功能,这个功能有点快,但仍然有很多不足之处(我仍然会遇到类型转换惩罚等)。

int inline fasterfloor( const float x ) { return x > 0 ? (int) x : (int) x - 1; }

我已经看到了使用内联汇编完成的一些技巧,但似乎没有任何工作完全正确或有任何显着的速度提升。

有没有人知道处理这种情况的任何技巧?

9 个答案:

答案 0 :(得分:11)

所以你想要一个非常快速的float-> int转换? AFAIK int->浮点转换很快,但至少在MSVC ++上,float-> int转换调用一个小的辅助函数ftol(),它执行一些复杂的操作以确保完成符合标准的转换。如果你不需要这样严格的转换,你可以做一些程序集hackery,假设你在x86兼容的CPU上。

这是一个快速float-to-int的函数,它使用MSVC ++内联汇编语法向下舍入(无论如何它应该给你正确的想法):

inline int ftoi_fast(float f)
{
    int i;

    __asm
    {
        fld f
        fistp i
    }

    return i;
}

在MSVC ++ 64位上,您需要一个外部.asm文件,因为64位编译器拒绝内联汇编。该函数基本上使用原始x87 FPU指令加载浮点数(fld),然后将浮点数存储为整数(fistp)。 (注意警告:你可以通过直接调整CPU上的寄存器来改变这里使用的舍入模式,但是不要这样做,你会破坏很多东西,包括MSVC执行sin和cos!)

如果您可以在CPU上承担SSE支持(或者有一种简单的方法来制作支持SSE的代码路径),您也可以尝试:

#include <emmintrin.h>

inline int ftoi_sse1(float f)
{
    return _mm_cvtt_ss2si(_mm_load_ss(&f));     // SSE1 instructions for float->int
}

...它基本相同(加载浮点数然后存储为整数)但使用SSE指令,这些指令更快一些。

其中一个应该涵盖昂贵的float-to-int情况,任何int-to-float转换应该仍然很便宜。很抱歉在这里是微软特有的,但这是我做过类似表现工作的地方,我通过这种方式获得了巨大收益。如果可移植性/其他编译器是一个问题,你将不得不查看其他东西,但这些函数编译为两个指令,采用&lt; 5个时钟,而不是一个需要100多个时钟的辅助函数。

答案 1 :(得分:10)

老问题,但是我遇到了它,它让我感到很震惊,它没有得到令人满意的回答。

TL; DR:*不要使用内联汇编,内在函数或任何其他给定的解决方案!相反,使用快速/不安全的数学优化进行编译(&#34; -ffast-math -funsafe-math-optimizations -fno-math-errno&#34; in g ++)。 floor()之所以如此慢的原因是因为如果转换会溢出(FLT_MAX不适合任何大小的标量整数类型),它会改变全局状态,这也使得除非禁用严格的IEEE-754兼容性,否则无法进行向量化,你不应该依赖它。使用这些标志进行编译可以禁用问题行为。

一些评论:

  1. 带标量寄存器的内联汇编不可矢量化,这在使用优化进行编译时会严重影响性能。它还要求当前存储在向量寄存器中的任何相关值溢出到堆栈并重新加载到标量寄存器中,这违背了手动优化的目的。

  2. 使用SSE cvttss2si的内联汇编和您已经概述的方法在我的机器上实际上比使用编译器优化的简单for循环慢。 这很可能是因为如果允许编译器将整个代码块一起矢量化,编译器将分配寄存器并更好地避免管道停顿。对于像这样的一小段代码,几乎没有内部依赖链,几乎没有寄存器溢出的可能性,它比asm()包围的手动优化代码更糟糕。

  3. 内联汇编不可移植,在Visual Studio 64位版本中不受支持,并且极难阅读。本质上也有与上面列出的相同的警告。

  4. 所有其他列出的方式都是不正确的,这可能比缓慢更糟糕,并且它们在每种情况下都给出了这样的边际性能改进,而这并不能证明方法的粗糙性。 (int)(x + 16.0)-16.0是如此糟糕,我甚至不会触摸它,但你的方法也是错误的,因为它给出了-2(-1)。在数学代码中包含分支时,如果性能至关重要,标准库不能为您完成工作,那么这也是一个非常糟糕的主意。所以你的(不正确的)方式应该看起来更像((int)x) - (x <0.0),也许是中间的,所以你不必执行两次fpu移动。分支可能导致缓存未命中,这将完全否定性能的任何增加;另外,如果禁用了数学errno,那么转换为int是任何floor()实现的最大剩余瓶颈。如果你/真的/不关心获得负整数的正确值,它可能是一个合理的近似值,但除非你非常了解你的用例,否则我不会冒风险。

  5. 我尝试使用按位转换和通过位掩码进行舍入,就像SUN的newlib实现在fmodf中所做的那样,但是它需要很长时间才能正确运行并且在我的机器上慢了几倍,即使没有相关的编译器优化标志。很可能,他们为一些古老的CPU编写了代码,其中浮点运算相对非常昂贵且没有向量扩展,更不用说向量转换操作了;在任何常见架构AFAIK上都不再是这种情况。 SUN也是Quake 3使用的快速逆sqrt()例程的诞生地;现在在大多数架构上都有一个指令。微优化的最大缺陷之一是它们很快就会过时。

答案 2 :(得分:3)

你想要的操作可以使用fmod函数表示(fmodf表示浮点数而不是双精度数):

#include <math.h>
u = fmodf(u, 1.0f);

您的编译器可以以最有效的方式执行此操作,这是相当不错的。

或者,您对最后一位精度有多关注?你能否对你的负值设置下限,例如知道它们永远不会低于-16.0的东西?如果是这样的话,像这样的东西会为你节省一个条件,如果它不能用你的数据可靠地进行分支预测,这很可能是有用的:

u = (u + 16.0);  // Does not affect fractional part aside from roundoff errors.
u -= (int)u;     // Recovers fractional part if positive.

(就此而言,根据您的数据和您正在使用的处理器,如果其中很大一部分是负数但非常小的部分低于16.0,您可能会发现在执行您的数据之前添加16.0f条件int-casting为你提供了一个加速,因为它使你的条件可预测。或者你的编译器可能正在使用条件分支之外的其他东西,在这种情况下它没用;没有测试和查看生成的程序集很难说。)< / p>

答案 3 :(得分:2)

另一个愚蠢的想法,如果范围很小可能会起作用......

使用按位运算从float中提取指数,然后使用查找表查找从尾数中擦除不需要的位的掩码。使用它来查找楼层(擦除点下方的位)以避免重新正常化问题。

编辑我将此删除为“太傻了,加上+ ve与-ve问题”。因为它无论如何都被提升了,它被取消了,我会留给其他人来决定它是多么愚蠢。

答案 4 :(得分:2)

如果您使用的是Visual C ++,请选中“启用内部函数”编译器设置。如果启用它应该使大多数数学函数更快(包括楼层)。缺点是处理边缘情况(如NaN)可能不正确,但对于游戏,您可能不在乎。

答案 5 :(得分:1)

如果可能出现的值范围足够小,也许您可​​以二进制搜索最低值。例如,如果值-2&lt; = x&lt; 2可以发生...

if (u < 0.0)
{
  if (u < 1.0)
  {
    //  floor is 0
  }
  else
  {
    //  floor is 1
  }
}
else
{
  if (u < -1.0)
  {
    //  floor is -2
  }
  else
  {
    //  floor is -1
  }
}

我不保证这一点 - 我不知道比较的效率与地板相比如何 - 但它可能值得尝试。

答案 6 :(得分:0)

你的u,v值的最大输入范围是多少?如果它是一个相当小的范围,例如-5.0到+5.0,然后重复加/减1.0会更快,直到你进入范围内,而不是调用昂贵的功能,如地板。

答案 7 :(得分:0)

这个不能解决铸造成本,但在数学上应该是正确的:

int inline fasterfloor( const float x ) { return x < 0 ? (int) x == x ? (int) x : (int) x -1 : (int) x; }

答案 8 :(得分:0)

如果您循环并使用u和v作为索引坐标,而不是将浮点放置以获取坐标,请将float和int保持为相同的值并将它们一起递增。这将为您提供在需要时使用的相应整数。