避免对楼层的呼叫()

11 c++ 3d graphics optimization floor

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

u -= floor(u)
v -= floor(v)
Run Code Online (Sandbox Code Playgroud)

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

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

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

int inline fasterfloor( const float x ) { return x > 0 ? (int) x : (int) x - 1; }
Run Code Online (Sandbox Code Playgroud)

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

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

Ash*_*ain 11

所以你想要一个非常快速的float-> int转换?AFAIK int-> float转换速度很快,但至少在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;
}
Run Code Online (Sandbox Code Playgroud)

在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
}
Run Code Online (Sandbox Code Playgroud)

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

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

  • 请注意,x87现在已经过时了.此外,给出的两个函数都是截断,而不是floor. (2认同)

小智 10

老问题,但我遇到了它,它让我轻微的痉挛,它没有得到满意的回答.

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编写了代码,其中浮点运算相对非常昂贵且没有向量扩展,更不用说向量转换操作了; 在任何常见架构AFAIK上都不再是这种情况.SUN也是Quake 3使用的快速逆sqrt()例程的诞生地; 现在在大多数架构上都有一个指令.微优化的最大缺陷之一是它们很快就会过时.

  • 有人明白了。我希望有人可以牺牲代表来立即投票+10。 (2认同)

Pau*_*l R 0

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

  • 一些评论:这种类型的循环(其中结束条件取决于循环内计算的某些内容)通常无法流水线化,这对许多系统来说是一个很大的性能影响。相比之下,如果有周围的代码可以进行管道化,那么 int&lt;-&gt;float 转换很容易管道化。然而,一般来说,你无法通过理论获得此类问题的可靠答案。为了得到真正的答案,原始发布者需要使用典型数据对各个版本进行基准测试。 (3认同)