【问题标题】:comparing two sums of floating point values in C or C++在 C 或 C++ 中比较两个浮点值的总和
【发布时间】:2017-06-03 09:24:09
【问题描述】:

假设您有两组根据 IEEE754 实现的浮点变量,这意味着被视为根据标准中存在的公式计算的精确值。所有合法值都是可能的。集合中变量的数量可以是任意自然数。

在数学意义上比较精确地比较由所述变量表示的值的总和是一种好方法。由于域的性质,问题可以很容易地表示为将单个和与零进行比较。您可以忽略存在 NaN 或 Infinities 的可能性,因为它与核心问题无关。 (可以轻松独立地检查这些值,并以适合此问题的特定应用的方式对其进行操作。)

一种天真的方法是简单地求和和比较,或将一组值相加并减去另一组值。

    bool compare(const std::vector<float>& lhs, const std::vector<float>& rhs)
    {
        float lSum = 0.0f;
        for (auto value : lhs)
        {
            lSum += value;
        }
        float rSum = 0.0f;
        for (auto value : rhs)
        {
            rSum += value;
        }

        return lSum < rSum;
    }

很明显,幼稚的方法存在问题,正如在有关浮点运算的其他各种问题中提到的那样。大多数问题都与两个困难有关:

  • 浮点值相加结果因顺序而异
  • 某些值集合的某些添加顺序可能会导致中间溢出(计算的中间结果超出可用数据类型支持的范围)

    float small = strtof("0x1.0p-126", NULL);
    float big = strtof("0x1.8p126", NULL);
    
    std::cout << std::hexfloat << small + big - big << std::endl;
    std::cout << std::hexfloat << (big-2*small) + (big-small) + big - (big+small) - (big+2*small) << std::endl;
    

    此代码将导致0inf;这说明了排序如何影响结果。希望排序问题也不是微不足道的。

    float prev;
    float curr = 0.0f;
    
    do
    {
        prev = curr;
        curr += strtof("0x1.0p-126", NULL);
    } while (prev != curr);
    
    std::cout << std::hexfloat << curr << std::endl;
    

如果有足够的时间来实际完成计算,这段代码将导致0x1.000000p-102,而不是像天真预期的那样0x1.fffffep127(将curr初始化更改为`strtof(“0x1.fff000p-103”)将建议实际观察这一点。);这说明了加法的中间结果和特定加数之间的比例如何影响结果。

关于获得最佳精度的说法很多,例如。 this question.

目前的问题不同,我们不想最大化精度,但我们有一个定义明确的函数,需要精确实现。

虽然对于某些人认为它可能是有用的练习的想法充其量似乎存在争议,但请考虑以下场景:这些值集之间的比较可能是在各种环境中对整个数据集独立执行的其他操作的基石。某些系统的同步、完美运行可能取决于这种比较是否得到良好定义和确定性实施,无论加数顺序和特定架构是否实施 IEEE754。

这个,或者只是好奇。

在讨论中,Kahan summation algorithm 被提及为相关。然而,该算法是最小化错误的合理尝试。它既不保证正确的结果符号,也不独立于操作顺序(至少保证一致的结果,如果错误的话,对于集合的排列)。

最明显的解决方案之一是使用/实现定点算法,使用足够数量的位来准确表示每个可能的操作数值并保持准确的中间结果。

但是,也许这可以仅使用浮点运算以保证结果符号正确的方式来完成。如果是这样,则需要在解决方案中解决溢出问题(如上述示例之一所示),因为该问题具有特定的技术方面。

(以下是原始问题。)

我有两组多浮点(浮点或双精度)值。我想为这个问题提供一个完美的答案,哪个集合的总和更大。由于浮点运算中的伪影,在某些极端情况下,天真的方法的结果可能是错误的,具体取决于操作的顺序。更不用说简单的 sum 会导致溢出。 我无法为我提供任何努力,因为我只有模糊的想法,所有这些想法都很复杂,没有说服力。

【问题讨论】:

  • 如果有其他信息可以添加到这个问题中以明确它不是重复的,请将其编辑到问题中。
  • @szpanczyk 您之前所做的任何澄清 cmets 现在都丢失了,这就是我要求您编辑问题的原因。如果该问题的答案也回答了您的问题,则该问题可以被视为重复。
  • @SimonByrne 我只是说这是这个问题的公认解决方案。如果我们需要一个精确的解决方案,我们一开始就不会使用浮点数。
  • @JonathanMee 浮点数本身是精确的,只是它们的运算(算术、转换为十进制)是不精确的。正如我在上面的评论中提到的,有一些称为超级累加器的算法可用于计算精确的总和。
  • 这篇文章可能因为没有提出明确的问题而受到影响。这个网站曾经有过驳斥仅仅描述某人应该编写的程序的“问题”的历史。

标签: c++ c floating-point overflow rounding-error


【解决方案1】:

一种可能的方法是使用 superaccumulator 计算总和:这是一种用于计算浮点数的精确总和的算法。虽然这些想法已经存在了一段时间,但这个术语是一个相对较新的术语。

在某种意义上,您可以将其视为 Kahan 求和的扩展,其中顺序求和存储为值数组,而不仅仅是一对。然后主要的挑战就变成了如何在各种值之间分配精度。

一些相关论文和代码:

  • 是的。 K. 朱和 W. B. 海耶斯。 “算法 908:浮点流的在线精确求和”。 ACM Transactions on Mathematical Software (ACM TOMS),37(3):37:1-37:13,2010 年 9 月。doi:10.1145/1824801.1824815

    • 不幸的是,论文和代码位于付费墙后面,但这似乎是the C++ code
  • R。 M. Neal,“使用小型和大型超级累加器的快速精确求和”。 2015. arXiv:1505.05571

  • M. T. Goodrich, A. Eldawy “浮点数求和的并行算法”。 2016. arXiv:1605.05436

【讨论】:

【解决方案2】:

2 个浮点数相加得到的浮点数只是一个近似值。给定 i1i2 的总和,我们可以找到这样做会导致浮点求和出错:

i1 + i2 = i12
i12 - i2 = i~1
i1 - i~1 =

对于 n 个数字的总和,我们可以得出的最接近的 近似 将是计算 n 的误差 - 1 加法运算,然后对那些 n - 1 错误求和,再次取 n - 2时间>。您将重复此过程 n - 2 次,或者直到所有错误都达到 0.0

有几件事可以将误差计算速度提高到 0.0:

  1. 使用更大的浮点类型,例如long double
  2. 在求和之前对列表进行排序,以便将小数加到小数中,将大数加到大数中

现在您可以评估准确性对您的重要性。我会告诉你,在一般情况下,考虑到你得到的结果仍然是一个近似值,上述操作的计算成本是惊人的。

普遍接受的解决方案是Kahan's Summation,这是速度和精度之间的完美结合。 Kahan 不会将错误保留到求和的末尾,而是将其滚动到每个加法中,防止它的值升级到最高精度浮点范围之外。假设我们得到了vector&lt;long double&gt; i1,我们可以对其进行 Kahan 的求和,如下所示:

auto c = 0.0L;
const auto sum = accumulate(next(cbegin(i1)), cend(i1), i1.front(), [&](const auto& sum, const auto& input) {
    const auto y = input - c;
    const auto t = sum + y;

    c = t - sum - y;
    return t;
} ) - c;

【讨论】:

  • 你的意思是因为我需要做的事情代价高昂,我应该做点别的。
  • @szpanczyk 更具体地说,我是说将大量计算用于查找确切数字但使用浮点数进行评估是浪费精力。这就像测量皮米的切口,然后用电锯切割。测量到毫米就足够了。因为你不能用你的工具来表示这种精确度。
  • 你弄错的地方是操作的顺序。如果你用电锯切割,然后测量到毫米怎么办?无论如何,这个问题的基本原理是在修订版中提出的,你可以忽略它。
  • @szpanczyk 如果您说输入的浮点数已经非常准确并且您必须保持完美的准确性,那么唯一的方法就是定点数学,正如您所建议的那样。如果您有兴趣,您可能会发现 Dobb 博士关于定点的文章很有帮助:drdobbs.com/cpp/fixed-point-arithmetic-types-for-c/…
  • @DrewDormann 不,不是真的。如果您可以证明正和的误差始终为正,而负和的误差始终为负,那么它仍然适用于您的示例。 (我并不是说存在这样的算法,只是我不会立即将其忽略)。
【解决方案3】:

确定执行这种比较的一种可能性是创建一个用于定点算术的类,其精度等于使用的类型,并且对绝对值没有限制。

它可以是一个实现以下公共方法的类:

    FixedPoint(double d);
    ~FixedPoint();

    FixedPoint operator+(const FixedPoint& rhs);
    FixedPoint operator-(const FixedPoint& rhs);
    bool isPositive();

(每个支持的浮点类型都需要单独的构造函数。)

根据具体情况,实施将需要一组固定的bool,取决于构造或动态大小;可能是std::bitsetvector&lt;bool&gt; 或静态或动态bool 数组。

为了便于实施,我建议实施 2 的补码编码。

这是一个明显且非常昂贵的解决方案,如果此比较是某些系统的核心,则会损害性能。希望有更好的解决方案。

【讨论】:

    【解决方案4】:

    Post 最初也是 C 语言,因此我的代码适用于此。
    我现在看到帖子只是 C++,但我在下面看到的内容很少适用于 C++。

    简化为查找 FP 数列表之和的符号

    比较两组数字就像将第二组的否定附加到第一组,然后找到联合列表之和的符号。此标志映射到 2 个原始集合中的 &gt;==&lt;

    只执行精确的 FP 数学

    假设:FP 采用类似 IEEE 的数字,包括次正规数、基数 2,并且对于某些操作是精确的:

    1. 添加具有相同二进制指数和不同符号的a +b

    2. 0.5 &lt;= |x| &lt; 1.0 范围内的数字减去相同符号 0.5。

    3. ldexp*()(将数字分解为有效和指数部分)函数返回一个精确值。

    每个指数形成数组

    形成一个总和数组sums[],其值将永远是(0 or 0.5 &lt;= |sums[i]| &lt; 1.0),每个可能的指数一个,以及一些大于最大值的指数。较大的需要累积超过FP_MAX|total_sum|。这需要log2(SIZE_MAX) 更多的元素。

    将这组数字添加到sums[]

    对于数字集的每个元素,根据其二进制指数将其添加到相应的sums[]。这是关键,因为可以完全完成使用共同 FP 二进制指数添加相同符号和不同符号 FP 数。添加可能导致具有相同符号值的进位和具有不同符号值的取消 - 这是已处理的。不需要对传入的一组数字进行排序。

    标准化sum[]

    对于ones[] 上的每个元素,确保减少任何不是0.5、0.0 或-0.5 的值,将剩余部分添加到更小的ones[]

    检查sum[] 的最高有效数字

    最重要的(非零)one[s] 是结果的符号。


    以下代码使用float 作为集合的FP 类型来执行任务。一些并行计算是使用double 完成的,以检查是否正常,但不参与float 计算。

    最后的标准化步骤通常会迭代两次。即使是最坏的情况,我怀疑也会迭代 float 符号的二进制宽度,大约 23 次。

    解决方案似乎大约是O(n),但确实使用了一个大小与 FP 的指数范围差不多的数组。

    #include <assert.h>
    #include <stdbool.h>
    #include <float.h>
    #include <stdio.h>
    #include <time.h>
    #include <stdint.h>
    #include <stdlib.h>
    #include <math.h>
    
    #if RAND_MAX/2 >= 0x7FFFFFFFFFFFFFFF
    #define LOOP_COUNT 1
    #elif RAND_MAX/2 >= 0x7FFFFFFF
    #define LOOP_COUNT 2
    #elif RAND_MAX/2 >= 0x1FFFFFF
    #define LOOP_COUNT 3
    #elif RAND_MAX/2 >= 0xFFFF
    #define LOOP_COUNT 4
    #else
    #define LOOP_COUNT 5
    #endif
    
    uint64_t rand_uint64(void) {
      uint64_t r = 0;
      for (int i = LOOP_COUNT; i > 0; i--) {
        r = r * (RAND_MAX + (uint64_t) 1u) + ((unsigned) rand());
      }
      return r;
    }
    
    typedef float fp1;
    typedef double fp2;
    
    fp1 rand_fp1(void) {
      union {
        fp1 f;
        uint64_t u64;
      } u;
      do {
        u.u64 = rand_uint64();
      } while (!isfinite(u.f));
      return u.f;
    }
    
    int pre = DBL_DECIMAL_DIG - 1;
    
    
    void exact_add(fp1 *sums, fp1 x, int expo);
    
    // Add x to sums[expo]
    // 0.5 <= |x| < 1
    // both same sign.
    void exact_fract_add(fp1 *sums, fp1 x, int expo) {
      assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
      assert(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0);
      assert((sums[expo] > 0.0) == ( x > 0.0));
    
      fp1 half = x > 0.0 ? 0.5 : -0.5;
      fp1 sum = (sums[expo] - half) + (x - half);
      if (fabsf(sum) >= 0.5) {
        assert(fabsf(sums[expo]) < 1.0);
        sums[expo] = sum;
      } else  {
        sums[expo] = 0.0;
        if (sum) exact_add(sums, sum, expo);
      }
      exact_add(sums, half, expo+1);  // carry
    }
    
    // Add  x to sums[expo]
    // 0.5 <= |x| < 1
    // differing sign
    void exact_fract_sub(fp1 *sums, fp1 x, int expo) {
      if(!(fabsf(x) >= 0.5 && fabsf(x) < 1.0)) {
        printf("%d %e\n", __LINE__, x);
        exit(-1);
      }
      assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
      assert((sums[expo] > 0.0) != ( x > 0.0));
      fp1 dif = sums[expo] + x;
      sums[expo] = 0.0;
      exact_add(sums, dif, expo);
    }
    
    // Add x to sums[]
    void exact_add(fp1 *sums, fp1 x, int expo) {
      if (x == 0) return;
      assert (x >= -FLT_MAX && x <= FLT_MAX);
      //while (fabsf(x) >= 1.0) { x /= 2.0; expo++; }
      while (fabsf(x) < 0.5) { x *= (fp1)2.0; expo--; }
      assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
    
      if (sums[expo] == 0.0) {
        sums[expo] = x;
        return;
      }
      if(!(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0)) {
        printf("%e\n", sums[expo]);
        printf("%d %e\n", expo, x);
        exit(-1);
      }
      assert(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0);
      if ((sums[expo] > 0.0) == (x > 0.0)) {
        exact_fract_add(sums, x, expo);
      } else {
        exact_fract_sub(sums, x, expo);
      }
    }
    
    void exact_add_general(fp1 *sums, fp1 x) {
      if (x == 0) return;
      assert (x >= -FLT_MAX && x <= FLT_MAX);
      int expo;
      x = frexpf(x, &expo);
      exact_add(sums, x, expo);
    }
    
    void sum_of_sums(const char *s, const fp1 *sums, int expo_min, int expo_max) {
      fp1 sum1 = 0.0;
      fp2 sum2 = 0.0;
      int step = expo_max >= expo_min ? 1 : -1;
      for (int expo = expo_min; expo/step <= expo_max/step; expo += step) {
        sum1 += ldexpf(sums[expo], expo);
        sum2 += ldexp(sums[expo], expo);
      }
      printf("%-20s = %+.*e %+.*e\n", s, pre, sum2, pre, sum1);
    }
    
    
    int test_sum(size_t N) {
      fp1 a[N];
      fp1 sum1 = 0.0;
      fp2 sum2 = 0.0;
      for (size_t i = 0; i < N; i++) {
        a[i] = (fp1) rand_fp1();
        sum1 += a[i];
        sum2 += a[i];
      }
      printf("%-20s = %+.*e %+.*e\n", "initial  sums", pre, sum2, pre, sum1);
    
      int expo_min;
      int expo_max;
      frexpf(FLT_TRUE_MIN, &expo_min);
      frexpf(FLT_MAX, &expo_max);
      size_t ln2_size = SIZE_MAX;
      while (ln2_size > 0) {
        ln2_size >>= 1;
        expo_max++;
      };
      fp1 sum_memory[expo_max - expo_min + 1];
      memset(sum_memory, 0, sizeof sum_memory);  // set to 0.0 cheat
      fp1 *sums = &sum_memory[-expo_min];
    
      for (size_t i = 0; i<N; i++)  {
        exact_add_general(sums, a[i]);
      }
      sum_of_sums("post add  sums", sums, expo_min,  expo_max);
    
      // normalize
      int done;
      do {
        done = 1;
        for (int expo = expo_max; expo >= expo_min; expo--) {
          fp1 x = sums[expo];
          if ((x < -0.5) || (x > 0.5)) {
            //printf("xxx %4d %+.*e ", expo, 2, x);
            done = 0;
            if (x > 0.0) {
              sums[expo] = 0.5;
              exact_add(sums, x - (fp1)0.5, expo);
            } else {
              sums[expo] = -0.5;
              exact_add(sums, x - -(fp1)0.5, expo);
            }
          }
        }
        sum_of_sums("end  sums", sums, expo_min,  expo_max);
      } while (!done);
    
      for (int expo = expo_max; expo >= expo_min; expo--) {
        if (sums[expo]) {
          return (sums[expo] > 0.5) ? 1 : -1;
        }
      }
      return 0;
    }
    
    #define ITERATIONS 10000
    #define MAX_NUMBERS_PER_SET 10000
    int main() {
      unsigned seed = (unsigned) time(NULL);
      seed = 0;
      printf("seed = %u\n", seed);
      srand(seed);
    
      for (unsigned i = 0; i < ITERATIONS; i++) {
        int cmp = test_sum((size_t)rand() % MAX_NUMBERS_PER_SET + 1);
        printf("Compare %d\n\n", cmp);
        if (cmp == 0) break;
      }
      printf("Success");
      return EXIT_SUCCESS;
    }
    

    在一定程度上也可以处理无穷大和 NaN。

    【讨论】:

    • 感谢您的准确回答。 c 标签已被其他人删除,这是发生的整个“全知全能”交易的一部分。
    • 就性能而言,这个答案似乎比我的要好得多。我会再等一会儿,看看是否会出现其他答案。
    • thisthat 之间的区别是 1) log2(FLT_MAX) - log2(FLT_TRUE_MIN) + log2(SIZE_MAX) 与待定 log2(superaccumulator) 的显式精度要求 2) 拥有数组 sums[] 与非常 宽超级累加器和 3) 现实与概念。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-10
    • 2011-10-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多