为什么我的代码中看到不一致?

时间:2017-11-20 16:51:18

标签: c floating-point openmp floating-accuracy reliability

灵感来自“只有40亿个花车 - 所以全部测试”我写了下面的代码:

#include<stdio.h>
#include<limits.h>
#include<math.h>
int main(){
    unsigned int i = 0;
    float        f = 0;
    float        r = 1;
    for(i = 0; i < UINT_MAX ; i++){
        f = *(float *)&i;
        r = pow(sin(f),2)+pow(cos(f),2);
        if (fabs(r-1)> 1/pow(2.0,20)){
            printf("hex value 0x%.8X bad value  0x%.8X \n",
                   *(unsigned int *)&f,*(unsigned int *)&r);
        }
        else {
            printf("hex value 0x%.8X good value  0x%.8X \n",
                   *(unsigned int *)&f,*(unsigned int *)&r);
        }
    }
    return 0;
}

我运行它(实际上只有几秒钟和OpenMP的并行版本),然后我输出了输出。每千个花车大约有1个失败!

math.h有多好?

我正在使用MacOS High Sierra。

[edit]澄清:

在阅读了评论和答案之后,我谦卑地承认我的问题不是很好,并且反映了C(2000 loc)非常环保。为了澄清,我正在使用C编译器(对于sinf与sin注释)并且代码被“清理”以在此处发布。 在阅读答案之后,我会认为任何挥之不去的不稳定都是由于我的错误造成的。以下仅供参考,是我的新代码,其中包含您的一些建议:

/usr/local/Cellar/gcc/7.2.0/bin/g++-7 -gdwarf-3 -Wall -Wshadow -g -DDEBUG -O0 floattest.c -o test.out -lm  -fopenmp

#include<stdio.h>
#include<limits.h>
#include<math.h>
#include<omp.h>
int main(){
    unsigned int i = 0;
    float        f = 0;
    float        r = 1;
#pragma omp parallel for private(i)
    for(i = 0; i < UINT_MAX ; i++){
        f = *(float *)&i;
        r = pow(sinf(f),2)+pow(cosf(f),2);
        if (fabs(r-1)> 1/pow(2.0,5)){
            printf("hex value 0x%.8X. Dec value %.10e. Bad value  0x%.8X \n",
                   *(unsigned int *)&f,f,*(unsigned int *)&r);
        }
    }
    return 0;
}

3 个答案:

答案 0 :(得分:3)

您正在尝试调查单精度浮点数量违反三角标识$\sin^2 x + \cos^2 x = 1$(抱歉,SO没有MathJax)的频率和频率。让我展示一个更好的方法:

  1. 为避免浪费时间并避免double rounding错误,您应该注意使用单精度数学;特别是,使用sinfcosf,而不是sincos;
  2. 您不应该使用pow[f](x, 2)来平方x,因为x*x会更快更准确(除非编译器可以优化pow[f](x,2)x*x,这不是你应该依赖的东西。)
  3. 你不应该把时间花在&#34;而不是数字&#34;值;
  4. 你应该使用C99函数nextafterldexp而不是类型 - 惩罚 - 除了不破坏类型别名规则,这很有可能更快;
  5. 最重要的是,你不应该为&#34;错误&#34;设置任意阈值。相反,你应该调查错误的分布。
  6. 以下是我的表现。 (注意:使用OpenMP,OpenMP 4.5阵列缩减以及一大堆C99功能。)

    #include <float.h>
    #include <inttypes.h>
    #include <limits.h>
    #include <math.h>
    #include <omp.h>
    #include <stdio.h>
    #include <string.h>
    
    #if FLT_RADIX != 2 || FLT_MIN_EXP != -125 || FLT_MAX_EXP != 128 || !defined INFINITY
    #error "This program assumes IEEE-compliant binary floating point"
    #endif
    
    // Including subnormals but *not* including infinities and NaNs, IEEE
    // singles can have exponents in the range [-149, 127].  Add two more
    // for exact zero and overflow.
    #define FLT_FULL_EXP_RANGE 279
    #define FLT_FULL_EXP_BIAS  148
    
    static inline void
    bin_error_in_trig_equality(float x, unsigned long *error_hist)
    {
      float s, c;
      int elog;
    
      s = sinf(x);
      c = cosf(x);
      elog = ilogbf(1.0f - (s*s + c*c));
      if (elog == FP_ILOGB0)
        error_hist[0]++;
      else if (elog == INT_MAX)
        error_hist[FLT_FULL_EXP_RANGE - 1]++;
      else
        error_hist[elog + FLT_FULL_EXP_BIAS]++;
    }
    
    int
    main(void)
    {
      // uint32_t is enough, because we are distributing fewer than
      // 2**32 values over these bins.
      uint32_t error_hist[FLT_FULL_EXP_RANGE] = { 0 };
    
      // Zero must be tested as a special case.
      bin_error_in_trig_equality(0.0f, error_hist);
    
      // Coarse grained parallel exhaustive test: each thread is responsible
      // for scanning the full mantissa range for one value of the exponent.
      // This covers all finite values except zero.
      #pragma omp parallel for reduction(+:error_hist)
      for (int i = -149; i <= 127; i++) {
        float lo = ldexpf(1.0f, i);
        float hi = ldexpf(1.0f, i+1);
        for (float x = lo; x < hi; x = nextafterf(x, hi)) {
          bin_error_in_trig_equality(x, error_hist);
          bin_error_in_trig_equality(-x, error_hist);
        }
      }
    
      // Dump the histogram in a convenient format for graphing.
      printf("bin,count\n");
      if (error_hist[0])
        printf("0.0,%"PRIu32"\n", error_hist[0]);
      for (int i = 1; i < FLT_FULL_EXP_RANGE; i++) {
        if (error_hist[i])
          printf("2^%d,%"PRIu32"\n", i - FLT_FULL_EXP_BIAS, error_hist[i]);
      }
      return 0;
    }
    

    当我运行这个程序时(在x86-64上,使用GNU libc提供的数学库 2.24),这是它的输出,手动对齐列:

    bin,        count
    0.0,   3633564531
    2^-24,  553412596
    2^-23,   91212952
    

    [来自Eric Postpischil]使用Apple LLVM 8.1.0,macOS 10.12.6和MacBookPro13,3运行相同的源代码:

    bin         count
    0.0,   3744628459
    2^-24,  431565466
    2^-23,  101996154
    

    错误始终存在于此并非意外 2 -24 或2 -23 箱;那些对应于IEEE单精度尾数的最低有效两位。

    该表的解释是:对于所有可能的有限单精度值的大约85%,标识是准确的;对于另外13%左右,错误不超过一unit in the last place(通常缩写为&#34; ulp&#34;);对于剩余的2.1%的值,误差不超过4个ulps。

    我认为这些是通用数学库的可接受错误率。 Transcendental functions are intrinsically hard to compute accurately;你需要使用任意精度数学来使每个值完美地舍入(并且工业级计算机代数引擎将完全实现这一点)。

    顺便提一下,这个程序在当前一代只需要12秒的挂钟时间,而不是基于英特尔的顶级笔记本电脑,所以观察到&#34; There are only four billion floats, test them all&# 34;自2014年以来,它变得更加真实。

答案 1 :(得分:1)

macOS中使用的sincos的来源是here 1 (注意:开头时有关这些是初步版本的评​​论会出现过时。)他们在数学上精确的结果的几个ULP内返回结果。鉴于此,它们的平方和应始终在1的ULP内。在此标度下,double ULP为2 -53 。因此,不应该出现大于2 -20 的错误。

实际上,当我使用Apple LLVM 8.1.0编译问题中的源代码并使用macOS 10.12.6在MacBookPro13,3上运行时,不会报告任何错误值。

因此,我认为OP错了;他们在这个问题中显示的源代码不是产生他们所说的失败的源代码。

脚注

1 我写了它们。

答案 2 :(得分:0)

  

每千个花车中大约有1个失败!

这些是Not-A-Numbers

许多float位模式通常代表Not-A-Number。那些float预计会“糟糕”。

sin(), cos() pow()质量因平台而异。质量较差的功能会导致更“糟糕”。

一个好的平台将有0个真正的,如下所示。

测试代码重新处理以处理各种缺点。

#include<stdio.h>
#include<limits.h>
#include<math.h>
int main(void) {
  unsigned good = 0;
  unsigned bad = 0;
  unsigned nan = 0;
  double ep = 1 / pow(2.0, 20);
  union {
    unsigned i;
    float f;
  } u = { 0 };
  do {
    float f = u.f;
    float r = (float) (pow(sin(f), 2) + pow(cos(f), 2));
    if (fabs(r - 1) <= ep) {
      good++;
    } else if (fabs(r - 1) > ep) {
      printf("%.20e\n", r);
      fflush(stdout);
      bad++;
    } else {
      nan++;
    }
  } while (++u.i);
  printf("good:%u bad:%u nan:%u\n", good, bad, nan);
  return 0;
}

我的结果

good:4278190080 bad:0 nan:16777216

因此,大约1/255是NaN。

请注意,OP使用double函数而不是sinf(), cosf(),...

同样提到的@Eric Postpischilhere,OP的代码使用double进行所有计算。因此,只有非常弱的<math.h>函数才会出现“坏”结果,因为double提供了额外的精度。

重新编写保险float数学导致以下内容。这将是对OP原始FP测试目标的更好评估。好sinf()cosf()预计是1.0 ULP中最好的答案。 OP方程的计算误差不应该与此相差甚远。

int main(void) {
  unsigned good = 0;
  unsigned bad = 0;
  unsigned nan = 0;
  union {
    unsigned i;
    float f;
  } u = { 0 };
  do {
    float f = u.f;
    if (isnan(f)) {
      nan++;
    } else {
      float s = sinf(f);
      float ss = powf(s, 2);
      float c = cosf(f);
      float cc = powf(c, 2);
      float r = ss + cc;
      if (r == 1) {
        good++;
      } else if (r > 1 || r < 1) {
        static double rmax = 0.0;
        if (fabs(r - 1) > rmax) {
          rmax = fabs(r - 1);
          printf("f:%.10e r:%.10e\n", f, rmax);
          fflush(stdout);
        }
        bad++;
      }
    }
  } while (++u.i);
  printf("good:%u bad:%u nan:%u\n", good, bad, nan);
  return 0;
}

输出。由于精度更高,许多结果都是“糟糕”,但 不好。

f:2.4414065410e-04 r:5.9604644775e-08
f:1.8584646285e-02 r:1.1920928955e-07
good:3730369702 bad:547820378 nan:16777214