1

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

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

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

所以,我陷入了两难境地:要么我只能调用双精度函数并避免这个问题(最终限制为每秒大约 1,300,000 次向量操作),要么我可以尝试提出其他方法。最终,我想要一种方法来清理逆触发函数的输入以处理边缘情况(对于 sqrt 来说这样做很简单:只需使用 abs)。分支不是一种选择,因为即使是单个条件语句也会增加太多开销,以至于失去任何性能提升。

那么,有什么想法吗?

编辑:有人对我使用双精度和浮点运算表示困惑。如果我将所有值实际存储在双倍大小的容器(即双类型变量)中,则该函数比将它们存储在浮点大小的容器中要快得多。但是,出于显而易见的原因,浮点精度三角运算比双精度三角运算更快。

4

3 回答 3

4

基本上,您需要找到一个数值稳定的算法来解决您的问题。这种事情没有通用的解决方案,需要针对您的具体情况使用诸如条件编号等概念(如果是各个步骤)来完成。如果根本问题本身是病态的,那实际上可能是不可能的。

于 2010-11-13T07:51:50.510 回答
4

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

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

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

但这很乱。限制域输入有点棘手,但更好。关键是使用有条件的移动操作符,它通常会做类似的事情

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

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

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

通过将比较运算符与按位 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 标量数学时,编译器更好,但不是很好,可以为三元组发出这种模式。您可以使用/arch:sse2MSVC 或-mfpmath=sseGCC 上的编译器标志来执行此操作。

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

于 2010-11-13T08:04:45.440 回答
1

您是否看过图形编程黑皮书,或者可能将计算交给您的 GPU?

于 2010-11-13T06:11:37.927 回答