std::fabs()的优化不佳?

13

最近,我在使用一个类似于以下代码的应用程序:

for (auto x = 0; x < width - 1 - left; ++x)
{
    // store / reset points
    temp = hPoint = 0;
    for(int channel = 0; channel < audioData.size(); channel++)
    {
        if (peakmode) /* fir rms of window size */
        {
            for (int z = 0; z < sizeFactor; z++)
            {
                temp += audioData[channel][x * sizeFactor + z + offset];
            }
            hPoint += temp / sizeFactor;
        }
        else /* highest sample in window */
        {
            for (int z = 0; z < sizeFactor; z++)
            {
                temp = audioData[channel][x * sizeFactor + z + offset];
                if (std::fabs(temp) > std::fabs(hPoint))
                hPoint = temp;
            }
        }
        .. some other code
    }
    ... some more code
}

这是在一个图形渲染循环中,以每秒50-100次的频率调用,在多个通道上具有高达192kHz的缓冲区。因此,大量数据通过最内部的循环运行,并且分析表明这是一个热点。

我想到可以将浮点数转换为整数并擦除符号位,然后仅使用临时变量将其转换回来。看起来类似于这样:

if ((const float &&)(*((int *)&temp) & ~0x80000000) > (const float &&)(*((int *)&hPoint) & ~0x80000000))
    hPoint = temp;

这样做可以将渲染时间缩短12倍,同时仍然产生相同且有效的输出。请注意,audiodata中的所有内容都经过处理以不包含NaNs/Infs/Denormals,并且仅具有[-1,1]的范围。

是否存在任何角落案例会导致此优化出现错误结果,或者为什么标准库函数没有像这样实现?我猜想这与处理非正常值有关?

e:浮点模型的布局符合IEEE标准,并且sizeof(float)== sizeof(int)== 4


4
这取决于浮点数的表示方法。换句话说,这是一个不太光彩的技巧。 - BЈовић
5
肮脏技巧(Dirty tricks)正是实现程序应该为您做的事情。它们就是实现程序——为特定平台和浮点表示等进行实现。请注意,不会改变原意,并尽可能通俗易懂。 - Puppy
2
“可能”不是软件工程中应该遇到的词语。让我们来检查一下。 - Richard Hodges
2
如果你想对优化着迷,可以参考这个链接: http://msdn.microsoft.com/en-us/library/bb513994(v=vs.90).aspx 我也会回复一条帖子,提供一些在可移植性方面优化函数的建议。 - Richard Hodges
4
太巧了,就在昨天我在我的电脑上对各种绝对值实现进行了一些定时:http://manwe.flamingdangerzone.com:9999/abs.html - R. Martinho Fernandes
显示剩余12条评论
6个回答

4
我想到了一种方法,可以将浮点数转换为整数并擦除符号位,然后只使用临时变量进行转换。但是这会违反严格别名规则,所以不行。
有没有任何边角情况,这种优化会导致错误的结果?
严格来说,这段代码会导致未定义的行为,因此它总是会给出错误的“结果”。不是绝对值的结果总是出乎意料或不正确,而是指如果程序具有未定义的行为,则无法推断程序的行为。
或者,为什么标准库函数没有像这样实现?
你的怀疑是正确的,处理非规格化值和其他异常值很棘手,stdlib函数也需要考虑这些值,而另一个原因仍然是未定义的行为。 如果您关心性能,一种(非)解决方案:

你可以使用联合(union)代替强制类型转换和指针。不幸的是,这只适用于C语言,而不是C++。这不会导致未定义行为,但仍然不可移植(尽管它可能在大多数平台上使用IEEE-754格式)。

union {
    float f;
    unsigned u;
} pun = { .f = -3.14 };

pun.u &= ~0x80000000;

printf("abs(-pi) = %f\n", pun.f);

但是,可以肯定的是,这可能比调用fabs()更快,也可能不会更快。唯一确定的是:它不总是正确的。


2
我以为使用临时变量可以规避别名问题,因为在任何时候我都没有多个引用类型指向同一原始数据? 如果我正在使用int指针迭代数据,那么我可以理解这个问题。 - Shaggi
4
工会和类型转换遇到的问题完全相同。你是通过一个表达式来访问对象的,而该表达式的类型与对象的类型不同(对象的类型为float,通过一个int类型的表达式进行访问)。 - MSalters
@MSalters:当然这是未定义行为,因为它依赖于“float”的内部表示。所以我认为标准对这段代码的规定并不是很相关;它旨在针对特定的实现(IEEE浮点数和合理的位操作)。 - Nate Eldredge
1
@NateEldredge:不,那个内部表示并不是导致UB的原因。它是UB是因为标准这样规定的。优化器会根据它进行积极的优化。例如,“在这两行代码之间你没有改变一个浮点数,所以我不需要重新加载寄存器中的浮点数”——如果您尝试通过int表达式修改浮点数,则会出现问题。这是真实世界的优化。 - MSalters
@MSalters:抱歉,我想到的是C语言,那里允许使用union技巧。 - Nate Eldredge
@MSalters 对不起,我有些没意识到这个问题是关于C++的。我已经相应地修改了我的答案。 - The Paramagnetic Croissant

4
你将浮点模式设置为符合IEEE标准。通常,当使用像--fast-math这样的开关时,编译器可以忽略IEEE的特殊情况,例如NaN、INF和denormal值。如果编译器还使用Intrinsic函数,它可能会生成相同的代码。
顺便说一下,如果你假设采用IEEE格式,那么在比较之前就不需要将它强制转换回float类型了。IEEE格式是很棒的:对于所有正有限值,当且仅当reinterpret_cast<int_type>(a) < reinterpret_cast<int_type>(b)时,a<b

2
在 MSVC 中,您可以在函数之前(即在全局范围内)添加 #pragma intrinsic(fabs) - MSalters
@MSalters /Oi不是启用所有内部函数,这样你就不需要编写pragma了吗? - Navin
1
这个符合IEEE标准的东西可以在http://goo.gl/pEK562(gcc explorer)上很好地看到。 - PlasmaHH
3
IEEE格式只在有符号数表示中非常好用。-0.5小于-0.125,但bf000000大于be000000。 - Vovanium
明确地说,这里可以工作是因为fabs返回正值。 - MSalters
显示剩余5条评论

3

你会期望fabs()在硬件中得到实现。毕竟,1980年就有了一个8087的指令。你不可能打败硬件。


2
即使在没有硬件实现的情况下,你也会期望它至少像某些人使用的技巧一样快速地实现。 - gnasher729

1
标准库函数的实现方式取决于实现,因此您可能会发现不同性能的标准库实现。我想在int不是32位的平台上可能会出现问题。最好使用int32_t (cstdint>)。就我的了解,std::abs以前是否被内联?还是您观察到的优化主要是由于函数调用的抑制?

它没有被内联,所以可能只是函数调用开销。 - Shaggi

1
一些关于重构如何提高性能的观察:
  • 如前所述,可以将 x * sizeFactor + offset 提取出内部循环

  • peakmode 实际上是一个开关,改变函数的行为 - 最好创建两个函数而不是在循环中测试开关。这有两个好处:

    1. 更易于维护
    2. 更少的本地变量和代码路径会妨碍优化。

  • temp 除以 sizeFactor 的操作可以延迟到 peakmode 版本的 channel 循环之外。

  • 每当更新 hPoint 时,都可以预先计算 abs(hPoint)

  • 如果 audioData 是向量的向量,则在 channel 循环体的开头获取对 audioData[channel] 的引用可能会带来一些性能好处,将在 z 循环中的数组索引减少到一维。

  • 最后,应用您认为合适的针对 fabs 计算的特定优化。您在此处所做的任何操作都会损害可移植性,因此这是最后的选择。


感谢您的建议!关于#2,该程序有几种不同的“模式”,如果我要为它们每个都分配一个不同的功能,这将导致32个不同(但非常相似)的函数,这实际上使得维护非常困难。我进行了测试,并且peakmode开关没有显着影响(可能是由于分支预测),因此减少函数数量是受欢迎的。 #1,#3和#4是正确的,我没有看到 - #5更加复杂,因为audioData的类型是模板化的。 - Shaggi
@Shaggi #2 - 很公平。 #5:auto& row = audioData[channel]; // 现在在循环中操作row[]。 - Richard Hodges
@Shaggi:如果你有32个相似的模式,你可能想调查一下是否可以使用模板来捕捉这种多样性。听起来你可能会有5个布尔参数。 - MSalters
@MSalters Close - 具有许多不同状态的对象!问题在于,整个系统可以轻松地模块化并编写得很好,但它确实有太多的性能惩罚。现在它是可维护性和性能之间的妥协,但不包括代码美感/可读性。幸运的是,这只是应用程序中小而性能关键的部分 :) - Shaggi
@Shaggi:至少在过去的15年中,模板没有性能开销。(我知道,我在1999年就测量过它们) - MSalters
@MSalters 我知道。但是我不确定除了将相关部分委托给lambda/functor(这些部分本来就可以在第一时间进行函数调用)之外,您如何在动态的、状态依赖的代码中利用编译时概念。 - Shaggi

0
在VS2008中,使用以下代码来跟踪hpoint的绝对值和hIsNeg是否为正数或负数,比使用fabs()快大约两倍:
int hIsNeg=0 ;
...
//Inside loop, replacing
//    if (std::fabs(temp) > std::fabs(hPoint))
//        hPoint = temp;
if( temp < 0 )
{
    if( -temp > hpoint )
    {
        hpoint = -temp ;
        hIsNeg = 1 ;
    }
}
else
{
    if( temp > hpoint )
    {
        hpoint = temp ;
        hIsNeg = 0 ;
    }
}
...
//After loop
if( hIsNeg )
    hpoint = -hpoint ;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接