在浮点精度成为问题之前,可以将多少个浮点加在一起

时间:2018-07-30 20:03:25

标签: c++ floating-point precision

我目前正在MS中记录某些帧时间,而不是滴答声。我知道这可能是个问题,因为我们将所有帧时间(以MS为单位)加在一起,然后除以帧数。由于浮点精度,可能导致不良结果。

将所有的滴答计数加在一起,然后最后一次转换为MS,将更有意义。

但是,我想知道少量样品的实际差异是多少?我希望有900-1800个样本。会不会是个问题?

我已经做了一个小例子,并在GCC 4.9.2上运行它:

// Example program
#include <iostream>
#include <string>

int main()
{
    float total = 0.0f;
    double total2 = 0.0f;

    for(int i = 0; i < 1000000; ++i)
    {
        float r = static_cast <float> (rand()) / static_cast <float> (RAND_MAX);
        total += r;
        total2 += r;
    }

    std::cout << "Total: " << total << std::endl;
    std::cout << "Total2: " << total2 << std::endl;
}

结果:

  

总计:500004总计2:500007

据我所知,使用一百万个值,我们并不会损失很多精度。尽管我不确定我写的是合理的测试还是实际测试我想测试的东西。

所以我的问题是,在精度成为问题之前,我可以添加多少个浮点数?我希望我的值介于1到60 MS之间。我希望最终精度在1毫秒内。我有900-1800的值。

值示例:15.1345f持续15毫秒。

3 个答案:

答案 0 :(得分:4)

Counterexample

使用以下有关问题陈述的假设(时间有效地以.06等值给出60毫秒),如果将.06转换为float并将其相加1800次,则计算结果为107.99884796142578125。这与数学结果108.000的差异超过了0.001。因此,计算结果有时会与数学结果相差超过1毫秒,因此在这些条件下无法实现问题中期望的目标。 (对问题陈述的进一步改进和替代的计算方法可能可以达到目标。)

原始分析

假设我们在[1,60]中有1800个整数值,并使用float转换为float y = x / 1000.f;,其中所有操作均使用IEEE-754基本32位二进制浮点数和正确的四舍五入。

从1到60到float的转换是准确的。除以1000的误差最多为½ULP(.06),即½•2 −5 •2 −23 = 2 −29 < / sup>。 1800这样的错误最多为1800•2 −29

当相加所得的float值时,每次相加的误差可能最多为1/2 ULP,其中ULP是当前结果的误差。为了进行宽松的分析,我们可以将其与最终结果的ULP绑定在一起,最终结果的ULP最多为1800•.06 = 108,ULP为2 6 •2 −23 = 2 −17 。因此,每个1799个附加项的误差最多为2 −17 ,因此,这些附加项的总误差最多为1799•2 −18

因此,除法和加法运算的总误差最多为1800•2 −29 + 1799•2 −18 ,约为.006866。

这是一个问题。我希望对加法中的错误进行更好的分析将使错误范围减半,因为它是从0到总数的算术级数,但是仍然留下了.003以上的潜在错误,这意味着总和可能为关闭几毫秒。

请注意,如果将时间加为整数,则最大的潜在总和为1800•60 = 108,000,这远低于float中无法表示的第一个整数(16,777,217)。在float中添加这些整数将没有错误。

.003的界限很小,足以对问题进行一些附加约束,并且可能进行一些附加分析,将其推到.0005以下,在这种情况下,计算结果将始终与正确的数学结果足够接近,以至于将计算结果四舍五入到最接近的毫秒数将产生正确的答案。

例如,如果知道时间在1到60毫秒之间,但总时间总是小于7.8秒,就足够了。

答案 1 :(得分:3)

尽可能减少浮点计算引起的错误

由于您已经描述了以毫秒为单位测量单个计时,因此最好在最终除以整数之前使用整数值累计这些计时:

std::milliseconds duration{};
for(Timing const& timing : timings) {
    //Lossless integer accumulation, in a scenario where overflow is extremely unlikely
    //or possibly even impossible for your problem domain
    duration += std::milliseconds(timing.getTicks());
}
//Only one floating-point calculation performed, error is minimal
float averageTiming = duration.count() / float(timings.size());

积累的错误是该场景的高度特殊

请考虑以下两种累积值的方法:

#include<iostream>

int main() {
    //Make them volatile to prevent compilers from optimizing away the additions
    volatile float sum1 = 0, sum2 = 0;
    for(float i = 0.0001; i < 1000; i += 0.0001) {
        sum1 += i;
    }
    for(float i = 1000; i > 0; i -= 0.0001) {
        sum2 += i;
    }

    std::cout << "Sum1: " << sum1 << std::endl;
    std::cout << "Sum2: " << sum2 << std::endl;
    std::cout << "% Difference: " << (sum2 - sum1) / (sum1 > sum2 ? sum1 : sum2) * 100 << "%" << std::endl;
    return 0;
}

在某些机器上(特别是没有IEEE754 float的机器),结果可能有所不同,但是在我的测试中,第二个值与第一个值相差3%,相差1300万。那可能非常重要。

像以前一样,最好的选择是最小化使用浮点值执行的计算的数量,直到将它们用作浮点值之前的最后一个可能步骤。这样可以将准确性损失降到最低。

答案 2 :(得分:1)

就它的价值而言,下面的一些代码演示了是的,在1800个项目之后,一次简单的累加可能会超过1毫秒,这是不正确的,但是Kahan求和保持了所需的准确性。

#include <iostream>
#include <iterator>
#include <iomanip>
#include <vector>
#include <numeric>

template <class InIt>
typename std::iterator_traits<InIt>::value_type accumulate(InIt begin, InIt end)
{
    typedef typename std::iterator_traits<InIt>::value_type real;
    real sum = real();
    real running_error = real();

    for (; begin != end; ++begin)
    {
        real difference = *begin - running_error;
        real temp = sum + difference;
        running_error = (temp - sum) - difference;
        sum = temp;
    }
    return sum;
}

int main()
{
    const float addend = 0.06f;
    const float count = 1800.0f;

    std::vector<float> d;

    std::fill_n(std::back_inserter(d), count, addend);

    float result = std::accumulate(d.begin(), d.end(), 0.0f);

    float result2 = accumulate(d.begin(), d.end());

    float reference = count * addend;

    std::cout << "   simple: " << std::setprecision(20) << result << "\n";
    std::cout << "    Kahan: " << std::setprecision(20) << result2 << "\n";
    std::cout << "Reference: " << std::setprecision(20) << reference << "\n";
}

对于此特定测试,似乎至少对于我尝试的输入值来说,双精度就足够了。但是,老实说,我仍然对此持怀疑态度,尤其是当穷举测试不合理时,更好的技术很容易获得。