求和具有大值范围的双精度数组:正确的算法

时间:2016-06-21 16:42:32

标签: c++ algorithm c++11 floating-point

我有一个算法,我需要在e-40到e + 40之间加上(很多时间)加倍的数字。

数组示例(从实际应用程序中随机转储):

-2.06991e-05 
7.58132e-06 
-3.91367e-06 
7.38921e-07 
-5.33143e-09
-4.13195e-11 
4.01724e-14 
6.03221e-17 
-4.4202e-20
6.58873 
-1.22257
-0.0606178 
0.00036508 
2.67599e-07 
0
-627.061
-59.048 
5.92985 
0.0885884
0.000276455 
-2.02579e-07

不言而喻,我知道这将导致舍入效应,我试图控制它:最终结果不应该在双倍的小数部分中有任何缺失信息,或者如果不可避免的结果应至少为n位精确(n定义)。最终结果需要5位数加上指数。

经过一番体面的思考后,我最终得到了以下算法:

  • 对数组进行排序,使最大的绝对值先到,最后接近于零。
  • 循环添加所有内容

这个想法是,在这种情况下,任何大值(负数和正数)的取消都不会影响后面的较小值。 简而言之:

  • (10e40 - 10e40)+ 1 = 1:结果如预期
  • (1 + 10e-40) - 10e40 = 0:不好

我最终使用了std :: multiset(我的PC上的基准测试速度提高了20%,而普通双打的速度提高了20倍 - 我的双打分辨率很好),使用std:fabs进行自定义排序功能。

它仍然很慢(完成整个过程需要5秒钟)而且我仍然有这样的感觉:“你错过了算法中的某些东西”。任何建议:

  1. 用于速度优化。有没有更好的方法来分类中间产品?对一组40个中间结果(通常)进行排序大约占总执行时间的70%。
  2. 错过了问题。是否还有机会丢失关键数据(应该是最终结果的小数部分)?
  3. 从更大的角度来看,我正在实现纯虚数变量的实系数多项式(电阻抗:Z(jw))。 Z是表示用户定义系统的大多项式,系数指数范围很远 “大”来自于将Zc1 = 1 / jC1w添加到Zc2 = 1 / jC2w:
        Zc1 + Zc2 =(C1C2(jw)^ 2 + 0(jw))/(C1 + C2)(jw)

    在这种情况下,对于纳米法(10e-9)的C1和C2,C1C2已经在10e-18(并且它只开始......)

    我的排序函数使用复数变量的曼哈顿距离(因为,我的是真实的或纯粹的虚构):

    struct manhattan_complex_distance
    {
            bool operator() (std::complex<long double> a, std::complex<long double> b)
            {
                return std::fabs(std::real(a) + std::imag(a)) > std::fabs(std::real(b) + std::imag(b));
            }
    };
    

    和我的多人组合:

    std:complex<long double> get_value(std::vector<std::complex<long double>>& frequency_vector)
    {
        //frequency_vector is precalculated once for all to have at index n the value (jw)^n. 
        std::multiset<std::complex<long double>, manhattan_distance> temp_list;   
        for (int i=0; i<m_coeficients.size(); ++i)
        {
            //   element of :       ℝ         *         ℂ
            temp_list.insert(m_coeficients[i] * frequency_vector[i]);
        }
        std::complex<long double> ret=0;
        for (auto i:temp_list)
        {
            // it is VERY important to start adding the big values before adding the small ones.
            // in informatics, 10^60 - 10^60 + 1 = 1; while 1 + 10^60 - 10^60 = 0. Of course you'd expected to get 1, not 0.
            ret += i;
        }
        return ret;
    }
    

    我的项目是启用c ++ 11(主要用于改进数学库和复数工具)

    ps:我重构代码使得易于阅读,实际上所有复合体和长双精度名称都是模板:我可以立即更改多项式类型或使用类作为常规多项式ℝ

3 个答案:

答案 0 :(得分:4)

建议GuyGreer,您可以使用Kahan summation

double sum = 0.0;
double c = 0.0;
for (double value : values) {
    double y = value - c;
    double t = sum + y;
    c = (t - sum) - y;
    sum = t;
}

编辑:您还应该考虑使用Horner's method来评估多项式。

double value = coeffs[degree];
for (auto i = degree; i-- > 0;) {
    value *= x;
    value += coeffs[i];
}

答案 1 :(得分:1)

对数据进行排序是正确的。但你绝对应该从最小幅度到最大幅度,而不是从最大到最小。从最大值到最小值求和,到达最小值时,将下一个值与当前总和对齐可能会导致下一个值的大部分或全部位“从结束时掉落”。从最小值到最大值进行求和,最小值有机会累积一个相当大的和,其中更多的位将进入最大值。结合Kahan求和,应该得到一个相当准确的总和。

答案 2 :(得分:1)

首先:让你的数学记录错误。使用错误感知类型替换您的双打,当您将两个双打加或乘时,它还会计算最大错误

这是您可以保证代码在合理快速的同时产生准确结果的唯一方法。

其次,请勿使用multiset。关联容器不用于排序,它们用于维护已排序的集合,同时能够有效地逐步添加或删除元素。

逐步添加/删除元素的能力意味着它是基于节点的,而基于节点意味着它通常很慢。

如果您只是想要一个已排序的集合,请先使用vector然后std::sort开始。

接下来,为了最大限度地减少错误,请保留正面和负面元素的列表。从零开始作为总和。现在选择正元素或负元素中的最小元素,使得总和和元素的总和最接近于零。

使用计算其误差范围的元素来执行此操作。

最后,确定您是否有5位数的精度。

这些错误传播双打应该尽可能早地在算法中使用。