如何计算反向trig(和sqrt)函数(在C中)的浮点运算中的舍入误差?

时间:2010-11-13 06:08:04

标签: c floating-point trigonometry math.h sqrt

我有一个相当复杂的函数,它采用几个双值来表示形式(幅度,纬度,经度)的3个空间中的两个向量,其中纬度和经度以弧度表示,并且是一个角度。该函数的目的是将第一个向量绕第二个向量旋转指定的角度并返回结果向量。我已经验证了代码在逻辑上是正确的并且有效。

该功能的预期用途是图形,因此不需要双精度;但是,在目标平台上,采用浮点数的trig(和sqrt)函数(具体为sinf,cosf,atan2f,asinf,acosf和sqrtf)在双精度上比在浮点数上工作得更快(可能因为计算这些值的指令实际上可能需要double;如果传递一个float,则该值必须强制转换为double,这需要将其复制到具有更多内存的区域 - 即开销。因此,函数中涉及的所有变量都是双精度。

问题在于:我正在尝试优化我的功能,以便每秒可以调用更多次。因此,我调用了sin,cos,sqrt等等调用这些函数的浮点版本,因为它们总体上提高了3-4倍的速度。这适用于几乎所有输入;但是,如果输入向量与标准单位向量(i,j或k)接近并行,则各种函数的舍入误差足以导致以后调用sqrtf或反向触发函数(asinf,acosf, atan2f)在这些函数的域之外传递勉强的参数。

所以,我陷入了这种困境:要么我只能调用双精度函数并避免问题(最终每秒限制大约1,300,000个向量运算),或者我可以试着想出其他的东西。最后,我想要一种方法来消除反向触发函数的输入以处理边缘情况(对于sqrt来说这是微不足道的:只使用abs)。分支不是一个选项,因为即使是单个条件语句也会增加很多开销,导致任何性能提升都会丢失。

那么,有什么想法吗?

编辑:有人对我使用双打与浮点操作表示混淆。如果我将所有值存储在双倍容器(I.E.双类型变量)中,而不是将它们存储在float-size容器中,则函数会快得多。但是,出于显而易见的原因,浮点精度触发操作比双精度触发操作更快。

3 个答案:

答案 0 :(得分:4)

基本上,您需要找到一个可以解决问题的numerically stable算法。对于这种事情没有通用的解决方案,如果个别步骤,则需要使用诸如condition number之类的概念来完成特定情况。如果潜在的问题本身就是病态的话,事实上可能是不可能的。

答案 1 :(得分:4)

单精度浮点固有地引入了错误。因此,您需要构建数学,以便所有比较都通过使用epsilon因子具有一定程度的“slop”,并且您需要清理具有有限域的函数的输入。

前者在分支时很容易,例如

bool IsAlmostEqual( float a, float b ) { return fabs(a-b) < 0.001f; } // or
bool IsAlmostEqual( float a, float b ) { return fabs(a-b) < (a * 0.0001f); } // for relative error

但那太乱了。钳位域输入有点棘手,但更好。关键是使用conditional move operators,这通常做类似

的事情
float ExampleOfConditionalMoveIntrinsic( float comparand, float a, float b ) 
{ return comparand >= 0.0f ? a : b ; }

在一个操作中,不会产生分支。

这取决于架构。在x87浮点单元上,您可以使用FCMOV conditional-move op执行此操作,但这很笨拙,因为它取决于先前设置的条件标志,因此速度很慢。此外,cmov没有一致的编译器内在函数。这就是为什么我们在可能的情况下避免使用x87浮点数来支持SSE2标量数学的原因之一。

通过将comparison operator与按位AND配对,可以更好地支持SSE中的条件移动。即使是标量数学,这也是首选:

// assuming you've already used _mm_load_ss to load your floats onto registers 
__m128 fsel( __m128 comparand, __m128 a, __m128 b ) 
{
    __m128 zero = {0,0,0,0};
    // set low word of mask to all 1s if comparand > 0
    __m128 mask = _mm_cmpgt_ss( comparand, zero );  
    a = _mm_and_ss( a, mask );    // a = a & mask 
    b = _mm_andnot_ss( mask, b ); // b = ~mask & b
    return _mm_or_ss( a, b );     // return a | b
    }
}

编译器在启用SSE2标量数学时为三元发出这种模式更好,但不是很好。您可以使用MSVC上的编译器标志/arch:sse2或GCC上的-mfpmath=sse来执行此操作。

在PowerPC和许多其他RISC架构上,fsel()是硬件操作码,因此通常也是编译器内在函数。

答案 2 :(得分:1)

您是否查看了Graphics Programming Black Book或将计算结果移交给您的GPU?