从 C 中的双精度中提取“真实”科学指数#

Extract the "real" scientific exponent from a double in C#

提问人:Matthew Layton 提问时间:5/18/2023 更新时间:5/23/2023 访问量:122

问:

在 C# 中,IEEE-754 数字(在本例中)可以以 10 为基数的科学记数法声明;例如:1.234e+9 (1234000000),但由于浮点数存储方式的限制,这些值的基数 2 浮点表示通常只是一个近似值。请注意下表:System.Double

科学记数法 IEEE-754 近似
1E-1型 0.10000000000000001
1E-2型 0.01
1E-3型 0.001
1E-4型 0.0001
1E-5型 1.00000000000000001E-05
1E-6型 9.99999999999999995E-07
1E-7型 9.999999999999999995E-08
1E-8型 1E-8型
1E-9型 1.0000000000000001E-09
1E-10型 1E-10型

忽略 IEEE-754 近似值的格式都不同这一事实。这些值都是直接从调试器中获取的。以不同的形式查看它们很有用,因为它有助于解释问题。

我想做的是从数字中提取指数。到目前为止,我有这个功能:

private static int GetScientificExponent(double value)
{
    return (int)double.Floor(double.Log10(double.Abs(value)));
}

结合上面的结果表,我们可以确定每种情况的科学指数:

科学记数法 IEEE-754 近似 科学指数
1E-1型 0.10000000000000001 -1
1E-2型 0.01 -2
1E-3型 0.001 -3
1E-4型 0.0001 -4
1E-5型 1.00000000000000001E-05 -5
1E-6型 9.99999999999999995E-07 -6
1E-7型 9.999999999999999995E-08 -7
1E-8型 1E-8型 -8
1E-9型 1.0000000000000001E-09 -9
1E-10型 1E-10型 -10

科学指数与每种情况的科学记数法都匹配,但它们并不与每种情况的 IEEE-754 近似值匹配,正如我在此表中所示。请注意异常值,以粗体突出显示:

科学记数法 IEEE-754 表示 科学指数 笔记
1E-1型 0.10000000000000001 -1 还行
1E-2型 0.01 -2 还行
1E-3型 0.001 -3 还行
1E-4型 0.0001 -4 还行
1E-5型 1.00000000000000001E-05 -5 还行
1E-6型 9.99999999999999995E-07 -6 E-6 与 E-7
1E-7型 9.999999999999999995E-08 -7 E-7 与 E-8
1E-8型 1E-8型 -8 还行
1E-9型 1.0000000000000001E-09 -9 还行
1E-10型 1E-10型 -10 还行

考虑到如果您以实际形式表示这些数字,而不是依赖 .NET的底层舍入机制(Dragon4左右,我被引导相信),那么你最终会得到这个:

科学记数法 以 10 为基数表示 IEEE-754 近似
1E-6型 0.000001 0.000000999999999999999954748111825886258685613938723690807819366455078125

值得注意的是,在以 10 为基数的 the 中,它位于小数点后第 6 位,而在 IEEE-754 近似中,它位于第 7 位,因此 -6 与 -7...这是有道理的。19

问题

给定我的函数,是否可以从近似值中获得指数?例如; 应该像现在一样返回,但应该返回,因为这是近似值的指数?GetScientificExponent1e-2-21e-6-7

为了完整起见,下面是我用来测试的代码:

using static System.Double;

internal static class Program
{
    private static void Main(string[] args)
    {
        double[] values = { 3e-1, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, 1e-7, 1e-8, 1e-9, 1e-10 };

        foreach (double value in values)
        {
            Console.WriteLine($"|{value:F10}|{GetScientificExponent(value)}|");
        }
    }

    private static int GetScientificExponent(double value)
    {
        return (int)Floor(Log10(Abs(value)));
    }
}
C# 浮点 IEEE-754

评论

0赞 canton7 5/18/2023
是的,这是可能的(必须如此,因为可以打印您想要的指数)。但是,代码很复杂(正如您所指出的,它是 Dragon4),并且它似乎是内部的,并且仅从格式化为字符串的代码中引用(最终在这里,从这里调用。double.ToString()
1赞 canton7 5/18/2023
老实说,最简单(也是最准确)的方法是格式化为字符串(使用 ),然后解析结果。.ToString("G17")
1赞 canton7 5/18/2023
如果你想要一个更令人满意的答案,请前往 C# discord 上的 #allow-unsafe-blocks -- 一群编译器/运行时人员在那里闲逛,包括 Tanner Gooding,他将能够明确地回答这个问题
0赞 Matthew Layton 5/18/2023
谢谢@canton7。我在推特上发了一个链接。
0赞 canton7 5/18/2023
你给谁发了推文?到目前为止,这个不和谐的频道是提出这类问题的最佳场所

答:

2赞 chux - Reinstate Monica 5/18/2023 #1

一种方法是让你的库的文本例程来执行所需的更高精度的数学运算。使用用户数学是不够的/不方便的。doubledouble

“诀窍”是以足够高的精度打印,以便接近 10 的幂的值不会导致指数在四舍五入时发生变化,然后简单地解析指数。

下面是 C 代码来说明。

特别是,在 的情况下,打印到 17 () 位有效数字是不够的。相反,代码打印重要的十进制数字。在某种程度上,使用更多数字几乎不会有任何损失。我推荐。除此之外,图书馆无需打印正确的数字并保持 IEEE-754 合规性。DBL_DECIMAL_DIG1e-79DBL_DECIMAL_DIG + 1DBL_DECIMAL_DIG + 3

示例代码

#include <float.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int GetScientificExponent(double value) {
  return (int) floor(log10(fabs(value)));
}

// 0.0, infinity and NAN return INT_MIN.  Adjust as desired.
int GetScientificExponent_alt1(double value) {
  #define PREC ((DBL_DECIMAL_DIG - 1) + 1 /* or + 3 */)
  if (isfinite(value)) {
    //       -   d   .         ddddd           e   -  eee \0
    char buf[1 + 1 + 1 + PREC + 1 + 1 + 5 + 1];
    sprintf(buf, "%.*e", PREC, value);
    printf("<%-30s>", buf);
    // Look for the exponent start
    const char *e = strchr(buf, 'e');
    if (e) {
      return atoi(e + 1);
    }
  }
  return INT_MIN;
}

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

void test1(double x) {
  printf("%30.*e %4d %4d\n", //
      DBL_DECIMAL_DIG * 3 / 2, x,
      GetScientificExponent_alt1(x), GetScientificExponent(x));
}

void test3(int power10) {
  double x = pow(10.0, power10);
  //test1(nextafter(x, +INFINITY));
  test1(x);
  //test1(nextafter(x, -INFINITY));
  //puts("");
}

int main() {
  for (int power10 = 0; power10 <= DBL_DECIMAL_DIG - DBL_MIN_10_EXP; power10++) {
    test3(-power10);
  }
}

输出

<1.00000000000000000e+00       >1.0000000000000000000000000e+00    0    0
<1.00000000000000006e-01       >1.0000000000000000555111512e-01   -1   -1
<1.00000000000000002e-02       >1.0000000000000000208166817e-02   -2   -2
<1.00000000000000002e-03       >1.0000000000000000208166817e-03   -3   -3
<1.00000000000000005e-04       >1.0000000000000000479217360e-04   -4   -4
<1.00000000000000008e-05       >1.0000000000000000818030539e-05   -5   -5
<9.99999999999999955e-07       >9.9999999999999995474811183e-07   -7   -6
<9.99999999999999955e-08       >9.9999999999999995474811183e-08   -8   -7
<1.00000000000000002e-08       >1.0000000000000000209225608e-08   -8   -8
<1.00000000000000006e-09       >1.0000000000000000622815915e-09   -9   -9
<1.00000000000000004e-10       >1.0000000000000000364321973e-10  -10  -10
<9.99999999999999939e-12       >9.9999999999999993949696928e-12  -12  -11
<9.99999999999999980e-13       >9.9999999999999997988664763e-13  -13  -12
<1.00000000000000003e-13       >1.0000000000000000303737456e-13  -13  -13
<9.99999999999999999e-15       >9.9999999999999999881930935e-15  -15  -14
<1.00000000000000008e-15       >1.0000000000000000777053999e-15  -15  -15
<9.99999999999999979e-17       >9.9999999999999997909778672e-17  -17  -16
<1.00000000000000007e-17       >1.0000000000000000715424241e-17  -17  -17
<1.00000000000000007e-18       >1.0000000000000000715424241e-18  -18  -18
<9.99999999999999975e-20       >9.9999999999999997524592684e-20  -20  -19
<9.99999999999999945e-21       >9.9999999999999994515327145e-21  -21  -20
<9.99999999999999908e-22       >9.9999999999999990753745223e-22  -22  -21
<1.00000000000000005e-22       >1.0000000000000000485967743e-22  -22  -22
<9.99999999999999960e-24       >9.9999999999999996043469801e-24  -24  -23
<9.99999999999999924e-25       >9.9999999999999992370049955e-25  -25  -24
<1.00000000000000004e-25       >1.0000000000000000384948697e-25  -25  -25
<1.00000000000000004e-26       >1.0000000000000000384948697e-26  -26  -26
<1.00000000000000004e-27       >1.0000000000000000384948697e-27  -27  -27
<9.99999999999999971e-29       >9.9999999999999997123254346e-29  -29  -28
<9.99999999999999943e-30       >9.9999999999999994320657418e-30  -30  -29
<1.00000000000000008e-30       >1.0000000000000000833364206e-30  -30  -30
...
<9.99999999999999958e-76       >9.9999999999999995765001370e-76  -76  -75
<9.99999999999999927e-77       >9.9999999999999992696817954e-77  -77  -76
<9.99999999999999927e-78       >9.9999999999999992696817954e-78  -78  -77
<9.99999999999999999e-79       >9.9999999999999999887872835e-79  -79  -78
<9.99999999999999999e-80       >9.9999999999999999887872835e-80  -80  -79
<9.99999999999999961e-81       >9.9999999999999996142531751e-81  -81  -80
<9.99999999999999961e-82       >9.9999999999999996142531751e-82  -82  -81
<9.99999999999999961e-83       >9.9999999999999996142531751e-83  -83  -82
...
<9.99999983659714433e-317      >9.9999998365971443346061921e-317 -317 -317
<1.00000023069253735e-317      >1.0000002306925373540838913e-317 -317 -317
<9.99998748495599830e-319      >9.9999874849559983034425877e-319 -319 -319
<9.99988867182683005e-320      >9.9998886718268300541337524e-320 -320 -320
<9.99988867182683005e-321      >9.9998886718268300541337524e-321 -321 -321
<9.98012604599318019e-322      >9.9801260459931801923666896e-322 -322 -322
<9.88131291682493088e-323      >9.8813129168249308835313759e-323 -323 -323
<9.88131291682493088e-324      >9.8813129168249308835313759e-324 -324 -324
<0.00000000000000000e+00       >0.0000000000000000000000000e+00    0 -2147483648

备选方案2

使用高精度数学。当精度超过至少 10 位时,OP 的原始方法也运行良好。long doubledouble

// Needs work for 0.0, infinity, NAN
private static int GetScientificExponent_alt2(double value) {
    long double dv = value;
    return (int)floor(log10(abs(dv)));
}

当然,如果不是特别精确的话,这和OP的原版有同样的麻烦。long double