11

我正在编写一段代码,我需要处理不一定在 0 到 1 范围内的 uvs(2D 纹理坐标)。例如,有时我会得到一个带有 au 组件的 uv,它是 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; }

我已经看到了一些通过内联汇编完成的技巧,但似乎没有任何技巧完全正确或有任何显着的速度改进。

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

4

9 回答 9

12

所以你想要一个真正快速的 float->int 转换?AFAIK int->float 转换很快,但至少在 MSVC++ 上,float->int 转换会调用一个小的辅助函数 ftol(),它会做一些复杂的事情来确保完成符合标准的转换。如果你不需要如此严格的转换,你可以做一些汇编黑客,假设你在一个 x86 兼容的 CPU 上。

这是一个快速浮点到整数的函数,它使用 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 指令,这有点快。

其中之一应该涵盖昂贵的浮点到整数的情况,并且任何整数到浮点的转换仍然应该很便宜。很抱歉在这里是微软特定的,但这是我做过类似性能工作的地方,我通过这种方式获得了巨大的收益。如果可移植性/其他编译器是一个问题,您将不得不查看其他内容,但是这些函数编译为可能需要 <5 个时钟的两条指令,而不是需要 100+ 个时钟的辅助函数。

于 2010-02-28T21:34:17.953 回答
12

老问题,但我遇到了它,它让我有点抽搐,它没有得到令人满意的回答。

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

一些备注:

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

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

  3. 内联汇编是不可移植的,在 Visual Studio 64 位版本中不受支持,并且非常难以阅读。内在函数受到相同的警告以及上面列出的警告。

  4. 所有其他列出的方法都是不正确的,这可以说比速度慢更糟糕,并且它们在每种情况下都给出了这样的边际性能改进,以至于不能证明该方法的粗糙度是合理的。(int)(x+16.0)-16.0 太糟糕了,我什至不会碰它,但你的方法也是错误的,因为它把 floor(-1) 设为 -2。在数学代码中包含分支也是一个非常糟糕的主意,因为它对性能至关重要,以至于标准库无法为您完成这项工作。所以你的(不正确的)方式应该看起来更像 ((int) x) - (x<0.0),也许有一个中间,所以你不必执行两次 fpu 移动。分支可能会导致缓存未命中,这将完全抵消性能的任何提升;此外,如果 math errno 被禁用,则转换为 int 是任何 floor() 实现的最大剩余瓶颈。

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

于 2017-01-10T03:17:46.163 回答
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,您可能会发现在执行条件 int 之前添加 16.0f-强制转换为您提供了加速,因为它使您的条件可预测。或者您的编译器可能使用条件分支以外的其他东西来执行此操作,在这种情况下它没有用;如果不测试和查看生成的程序集,很难说。)

于 2010-02-28T21:23:21.207 回答
2

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

使用按位运算从浮点数中提取指数,然后使用查找表查找从尾数中擦除不需要的位的掩码。使用它来查找地板(擦除该点下方的位)以避免重新归一化问题。

编辑我将其删除为“太傻了,加上 +ve 与 -ve 的问题”。既然它无论如何都得到了投票,它没有被删除,我将把它留给其他人来决定它有多愚蠢。

于 2010-02-28T19:53:53.873 回答
2

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

于 2010-02-28T19:56:19.727 回答
0

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

于 2010-02-28T19:39:56.267 回答
0

如果可能出现的值范围足够小,也许您可​​以对底值进行二分搜索。例如,如果值 -2 <= x < 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
  }
}

我对此不做任何保证——我不知道比较效率与地板相比如何——但它可能值得一试。

于 2010-02-28T19:45:51.020 回答
0

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

于 2014-04-23T15:07:09.090 回答
0

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

int inline fasterfloor( const float x ) { return x < 0 ? (int) x == x ? (int) x : (int) x -1 : (int) x; }
于 2011-01-10T15:29:28.303 回答