仍然值得尝试在C中为sqrt()创建优化吗?

时间:2009-05-28 18:58:59

标签: c performance optimization

用于创建sqrt()的更快实现的旧技巧(查找表,近似函数)是否仍然有用,或者是现代编译器和硬件的默认实现?

8 个答案:

答案 0 :(得分:16)

规则1:优化前的配置文件

在投入任何努力以确保您能够击败优化器之前,您必须对所有内容进行分析并发现瓶颈所在的位置。一般来说,sqrt()本身不太可能是您的瓶颈。

规则2:在替换标准函数之前替换算法

即使sqrt()是瓶颈,那么仍然有可能存在算法方法(例如按长度平方排序距离,无需调用任何数学函数即可轻松计算),这可以消除需求首先致电sqrt()

如果您不采取任何其他措施,编译器会为您做什么

许多现代C编译器都愿意以更高的优化级别内联CRT函数,使得自然表达包括尽可能快地调用sqrt()

特别是,我检查了MinGW gcc v3.4.5,它用一个内嵌代码替换了对sqrt()的调用,该内联代码改组了FPU状态,核心使用了FSQRT指令。由于C标准与IEEE 754浮点交互的方式,它必须遵循FSQRT一些代码来检查异常情况并从运行时库调用真实sqrt()函数因此,库可以按照标准的要求处理浮点异常。

使用sqrt()内联并在更大的所有double表达式的上下文中使用,结果尽可能高效,因为符合标准并保持完全精度。

对于编译器和目标平台的这种(非常常见的)组合,并且不了解用例,这个结果非常好,代码清晰可维护。

在实践中,任何技巧都会使代码不那么清晰,而且可能的维护也不那么容易。毕竟,您宁愿维护(-b + sqrt(b*b - 4.*a*c)) / (2*a)还是内联汇编和表格的不透明块?

此外,在实践中,您通常可以指望编译器和库作者充分利用您平台的功能,并且通常比您对优化的细微之处了解更多。

但是,在极少数情况下,可能会做得更好。

在计算中有一个这样的场合,你知道你真正需要多少精度,并且知道你不依赖于C标准的浮点异常处理,而且可以与硬件平台提供的东西相提并论。

编辑:我重新安排了一些文字,重点放在Jonathan Leffler在评论中建议的分析和算法。谢谢,乔纳森。

Edit2:修正kmm犀利的眼睛所发现的二次范例中的优先错误。

答案 1 :(得分:4)

Sqrt在大多数系统上基本没有变化。这是一个相对较慢的操作,但总系统速度有所提高,因此可能不值得尝试使用“技巧”。

使用近似的(次要)增益来优化它的决定实际上取决于你。现代硬件已经消除了对这些类型牺牲(速度与精度)的一些需求,但在某些情况下,这仍然是有价值的。

我会使用性能分析来确定这是否“仍然有用”。

答案 2 :(得分:3)

如果您已经证明代码中对 sqrt()的调用是分析器的瓶颈,则可能值得尝试创建优化版本。否则这是浪费时间。

答案 3 :(得分:2)

通常可以安全地假设标准库开发人员非常聪明,并且编写了高性能代码。一般来说,你不太可能匹配它们。

所以问题就变成了,你知道什么会让你做得更好吗?我不是要问关于计算平方根的特殊算法(标准库开发人员也知道这些算法,如果它们一般都值得,他们已经使用过它们了),但是你有关于您的用例,会改变这种情况吗?

您只需要有限的精度吗?如果是这样,与标准库版本相比,您可以加快速度,这必须准确。

或者您知道您的应用程序总是在特定类型的CPU上运行吗?然后,您可以查看CPU的sqrt指令的效率,并查看是否有更好的替代方案。当然,这样做的缺点是如果我在另一个CPU上运行你的应用程序,你的代码可能会比标准的sqrt()慢。

您可以在代码中做出假设,标准库开发人员不能这样做吗?

您不太可能能够找到更好的解决方案来解决问题“实现标准库sqrt的有效替换”。

但是你可能能够找到问题的解决方案“为这个特定情况实现有效的平方根功能”。

答案 4 :(得分:2)

这可能是计算平方根的最快方法:

float fastsqrt(float val)  {
        union
        {
                int tmp;
                float val;
        } u;
        u.val = val;
        u.tmp -= 1<<23; /* Remove last bit so 1.0 gives 1.0 */
        /* tmp is now an approximation to logbase2(val) */
        u.tmp >>= 1; /* divide by 2 */
        u.tmp += 1<<29; /* add 64 to exponent: (e+127)/2 =(e/2)+63, */
        /* that represents (e/2)-64 but we want e/2 */
        return u.val;
}

wikipedia article


这可能是计算平方根的最快方法。假设最多0.00175228错误。

float InvSqrt (float x)
{
    float xhalf = 0.5f*x;
    int i = *(int*)&x;
    i = 0x5f3759df - (i>>1);
    x = *(float*)&i;
    return x*(1.5f - xhalf*x*x);
}

这是(非常粗略地)比(float)(1.0/sqrt(x))

快约4倍

wikipedia article

答案 5 :(得分:1)

为什么不呢?你可能学到了很多东西!

答案 6 :(得分:0)

由于现代计算机的设计方式,我发现很难相信sqrt功能是您应用程序的瓶颈。假设这不是一个关于某些疯狂的低端处理器的问题,那么你需要以极高的速度访问CPU缓存之外的内存,所以除非你算法在很少的数字上进行数学运算(足够他们所有基本上适合L1和L2缓存)你不会注意到优化任何算术的速度。

答案 7 :(得分:0)

即使是现在我仍然觉得它很有用,尽管这是为了响应变形网格而每帧标准化一百万个+矢量的背景。

那就是说,我通常不会创建自己的优化,而是依赖于作为SIMD指令提供的反平方根的粗略近似值:rsqrtps。如果你愿意牺牲精度来提高速度,这对于加速一些真实案例来说仍然非常有用。使用rsqrtps实际上可以减少整个操作,包括将顶点法线变形和标准化几乎一半的时间,但是以结果的精确度为代价(也就是说,这种方式几乎无法被注意到人眼)。

我还发现快速反向sqrt通常不正确地归功于John Carmack在标量情况下仍能提高性能,尽管我现在并没有太多使用它。如果你愿意牺牲精确度,通常可以自然地获得一些速度提升。也就是说,如果你不想牺牲速度的精确度,我甚至不会试图击败C sqrt

如果你想要超越标准的实现,你通常必须牺牲解决方案的一般性(比如它的精度),并且这往往适用于它是一个数学函数还是,例如malloc。我可以轻松地使用一个适用于非常特定的上下文的缺少线程安全的狭义适用的免费列表来击败malloc。用通用分配器来击败它是另一回事,它可以分配可变大小的内存块并在任何给定时间释放它们中的任何一个。