【问题标题】:Float inputs for which sinf and sin return different results?浮点输入哪个 sinf 和 sin 返回不同的结果?
【发布时间】:2021-11-26 20:13:18
【问题描述】:

我试图从math.h 中了解有关sinsinf 的一些信息。

我知道它们的类型不同:前者接受并返回doubles,后者接受并返回floats。

但是,如果我使用 float 参数调用 sin,GCC 仍会编译我的代码:

#include <stdio.h>
#include <math.h>

#define PI 3.14159265

int main ()
{
  float x, result;
  x = 135 / 180 * PI;
  result = sin (x);
  printf ("The sin of (x=%f) is %f\n", x, result);
  return 0;
}

默认情况下,所有编译都很好(即使使用 -Wall-std=c99-Wpedantic;我需要使用 C99)。 GCC 不会抱怨我将浮点数传递给sin。如果我启用-Wconversion,那么 GCC 会告诉我:

warning: conversion to ‘float’ from ‘double’ may alter its value [-Wfloat-conversion]
   result = sin (x);
            ^~~

所以我的问题是:是否有一个float 输入使用sin,就像上面一样,并且(隐式)将结果转换回float,将导致一个与使用获得的值不同的值sinf?

【问题讨论】:

  • 135 / 180 * PI 不会做你所期望的。请改用135.0 / 180 * PI
  • 对于result = sin (x); MSVC 警告:double 转换为float,可能会丢失数据
  • 由于 sinsinf 的大多数实现都没有正确舍入,并且它们未正确舍入的方式不同,因此任何答案只能特定于一种实现(sin 和 @ 987654347@).
  • 仅供参考,您发现的大多数不匹配可能是由于sinf 通常实现的精度接近float。例如,macOS 实现提供了如实四舍五入的结果——大多数结果都是正确四舍五入的(返回真实正弦的最接近的可表示值),但有些结果只是如实四舍五入(选择了两个周围的可表示值之一)。为此,正弦的计算误差小于float 格式的 1 ULP,然后四舍五入为 float...
  • ... 相比之下,sin 例程以更高的精度计算正弦,通常是 double 格式的几个 ULP。这意味着如果(float) sin(x)sinf(x) 不同,则前者通常是更好的结果。如果sinsinf 都返回正确的舍入结果,则(float) sin(x)sinf(x) 之间的差异将非常少见,仅发生在正弦导致双舍入错误的情况下(第一次舍入到double 将其推过float 舍入会改变的边界)。这是 2^29 中的一个。

标签: c floating-point precision trigonometry c99


【解决方案1】:

这个程序在我的机器上找到三个例子:

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    int i;
    float f, f1, f2;

    for(i = 0; i < 10000; i++) {
        f = (float)rand() / RAND_MAX;
        float f1 = sinf(f);
        float f2 = sin(f);

        if(f1 != f2) printf("jackpot: %.8f %.8f %.8f\n", f, f1, f2);
    }
}

我明白了:

jackpot: 0.98704159 0.83439910 0.83439904
jackpot: 0.78605396 0.70757037 0.70757031
jackpot: 0.78636044 0.70778692 0.70778686

【讨论】:

  • 既然没有那么多花车,你甚至可以做一个详尽的搜索......
【解决方案2】:

float 的精度约为十进制的 6 位有效数字,而 double 的精度约为 15。(这是近似值,因为它们是 二进制浮点 值而不是 十进制浮点)。

例如:double1.23456789 将变为 1.23456xxx 作为 float 其中 xxx 在这种情况下不太可能是 789

显然不是所有(实际上很少)double完全可以由float 表示,因此在向下转换时会改变值。

所以对于:

double a = 1.23456789 ;
float b = a ;
printf( "double: %.10f\n", a ) ;
printf( "float: %.10f\n", b ) ;

我的测试结果是:

double: 1.2345678900
float:  1.2345678806

正如您所见,float 在这种情况下实际上保留了 9 个有效数字,但绝不保证所有可能的值。

在您的测试中,由于rand() 的范围有限且有限,而且f 本身就是float,因此您限制了不匹配实例的数量。考虑:

int main()
{
    unsigned mismatch_count = 0 ;
    unsigned iterations = 0 ;
    for( double f = 0; f < 6.28318530718; f += 0.000001) 
    {
        float f1 = sinf(f);
        float f2 = sin(f);
        iterations++ ;
        if(f1 != f2)
        {
            mismatch_count++ ;
        }
    }
    printf("%f%%\n", (double)mismatch_count/iterations* 100.0);}

在我的测试中,大约 55% 的比较不匹配。将f 更改为float,不匹配减少到1.3%。

因此,在您的测试中,由于生成 f 的方法及其类型的限制,您几乎不会看到不匹配的情况。在一般情况下,问题要明显得多。

在某些情况下,您可能不会看到不匹配的情况 - 实现可能只是使用 sin() 和显式转换来实现 sinf()。编译器警告是针对将 double 隐式转换为 float 的一般情况,而不参考在转换之前执行的任何操作。

【讨论】:

    【解决方案3】:

    这将找到0.02 * M_PI 范围内的所有float input 值,其中(float)sin(input) != sinf(input)

    #include <stdio.h>
    #include <math.h>
    #include <float.h>
    
    #ifndef M_PI
    #define M_PI 3.14159265358979323846
    #endif
    
    int main(void)
    {
        for (float in = 0.0; in < 2 * M_PI; in = nextafterf(in, FLT_MAX)) {
            float sin_result = (float)sin(in);
            float sinf_result = sinf(in);
            if (sin_result != sinf_result) {
                printf("sin(%.*g) = %.*g, sinf(%.*g) = %.*g\n",
                       FLT_DECIMAL_DIG, in, FLT_DECIMAL_DIG, sin_result,
                       FLT_DECIMAL_DIG, in, FLT_DECIMAL_DIG, sinf_result);
            }
        }
        return 0;
    }
    

    在我的带有 glibc 2.32 的 amd64 Linux 系统上有 1020963 个这样的输入。

    【讨论】:

      【解决方案4】:

      但是,如果我使用浮点参数调用 sin,GCC 仍会编译我的代码:

      是的,这是因为它们在进入和退出 @ 时被隐式转换为 double(因为 sin() 需要浮点数),并返回到 float(因为 sin() 返回 double) 987654326@ 功能。请看下面为什么在这种情况下最好使用sinf(),而不是只有一个函数。

      您已包含math.h,它具有两个函数调用的原型:

      double sin(double);
      float sinf(float);
      

      因此,编译器知道要使用sin(),必须将float 转换为double,因此它在调用之前编译转换,并且编译 sin() 的结果中从 doublefloat 的转换。

      如果你没有 #include &lt;math.h&gt; 并且你忽略了编译器警告告诉你正在调用一个没有原型的函数 sin(),编译器也应该首先将 float 转换为 double(因为在未指定的参数上输入这是它必须进行的方式)并将double 数据传递给函数(在这种情况下假定返回int,这将引发严重的未定义行为

      如果您使用了sinf() 函数(具有正确的原型),并传递了float,则不应编译转换,浮点数按原样传递,不进行类型转换,返回值为分配给float 变量,也没有转换。所以一切顺利,无需转换,这是最快的代码。

      如果您使用了sinf() 函数(没有原型)并传递了float,则此浮点数将转换为double 并按原样传递给sinf(),从而导致未定义的行为。如果sinf() 以某种方式正确返回,int 结果(可能与计算有关,根据 UB)将转换为 float 类型(如果可能的话)并分配给结果价值。

      在上面提到的情况下,如果您在floats 上进行操作,最好使用sinf(),因为它需要更少的执行时间(它需要做的迭代更少,因为它们需要更少的精度)并且这两个转换(从floatdouble 以及从doublefloat)不必在编译器输出的二进制代码中编译。

      【讨论】:

        【解决方案5】:

        在某些系统中,float 上的计算比 double 上的计算快一个数量级。 sinf 的主要目的是在 float 的较低精度足以满足应用程序需求的情况下,允许在此类系统上有效地执行三角计算。将一个值转换为float,调用sin,并将结果转换为float,总是会产生一个与sinf相匹配或更准确(*)的值,并且在某些实现上实际上会成为实现sinf 的最有效方式。然而,在其他一些系统上,这种方法比使用专门设计的函数来评估 float 的正弦要慢一个数量级以上。

        (*) 请注意,对于 +/- π/2 范围之外的参数,对于 x 的精确指定值计算 sin(x) 的最数学准确方法可能不是计算调用的最准确方法代码想知道。如果应用程序计算 sinf(angle * (2.0f * 3.14159265f)),当 angle 为 0.5 时,使用函数 (double)3.1415926535897932385-(float)3.14159265f 可能比返回 sin(angle-(2.0f*3.14159265f)) 更“数学准确”,但后者会更准确地表示代码实际感兴趣的角度的正弦。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2019-10-29
          • 2012-07-26
          • 1970-01-01
          • 1970-01-01
          • 2022-01-16
          相关资源
          最近更新 更多