如何以完美的精度打印浮点值以供以后扫描?

How do I print a floating-point value for later scanning with perfect accuracy?

假设我有一个 floatdouble 类型的浮点值(即典型机器上的 32 或 64 位)。我想将这个值打印为文本(例如到标准输出流),然后在其他一些进程中将其扫描回来 - 如果我使用 C,则使用 fscanf(),或者可能使用 istream::operator>>() 如果我使用 C++。但是 - 我需要扫描的浮点数最终成为 exactly,与原始值相同(直到相同值的等效表示)。此外,打印的值应该易于阅读 - 对于人类来说 - 作为浮点数,即我不想打印 0x42355316 并将其重新解释为 32 位浮点数。

我应该怎么做?我假设(C 和 C++)的标准库是不够的,但也许我错了。我想足够多的小数位数可能能够保证低于精度阈值的错误 - 但这与保证 rounding/truncation 会按照我想要的方式发生是不一样的。

备注:

您可以使用 %a 格式说明符将值打印为十六进制浮点数。请注意,这与将 float 重新解释为整数并打印整数值不同。

例如:

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

int main()
{
    float x;
    scanf("%f", &x);
    printf("x=%.7f\n", x);

    char str[20];
    sprintf(str, "%a", x);
    printf("str=%s\n", str);

    float y;
    sscanf(str, "%f", &y);
    printf("y=%.7f\n", y);
    printf("x==y: %d\n", (x == y));

    return 0;
}

输入 4,输出:

x=4.0000000
str=0x1p+2
y=4.0000000
x==y: 1

输入 3.3,输出:

x=3.3000000
str=0x1.a66666p+1
y=3.3000000
x==y: 1

从输出中可以看出,%a 格式说明符以指数格式打印,尾数为十六进制,指数为十进制。然后可以将此格式直接转换回与相等性检查所证明的完全相同的值。

首先,您应该将 %a 格式与 fprintffscanf 一起使用。这就是它的设计目的,如果实现使用二进制 floating-point.

,C 标准要求它工作(再现原始数字)

否则,您应该打印一个至少有 FLT_DECIMAL_DIG 位有效数字的 float 和一个至少有 DBL_DECIMAL_DIG 位有效数字的 double。这些常量在 <float.h> 中定义并定义为:

… number of decimal digits, n, such that any floating-point number with p radix b digits can be rounded to a floating-point number with n decimal digits and back again without change to the value,… [b is the base used for the floating-point format, defined in FLT_RADIX, and p is the number of base-b digits in the format.]

例如:

    printf("%.*g\n", FLT_DECIMAL_DIG, 1.f/3);

或:

#define QuoteHelper(x)  #x
#define Quote(x)        QuoteHelper(x)
…
    printf("%." Quote(FLT_DECIMAL_DIG) "g\n", 1.f/3);

在 C++ 中,这些常量在 <limits> 中定义为 std::numeric_limits<Type>::max_digits10,其中 Typefloatdouble 或另一个 floating-point类型。

请注意,C 标准仅建议这样的 round-trip 通过十进制数字工作;它不需要它。例如,C 2018 5.2.4.2.2 15 在“推荐做法”标题下说:

Conversion from (at least) double to decimal with DECIMAL_DIG digits and back should be the identity function. [DECIMAL_DIG is the equivalent of FLT_DECIMAL_DIG or DBL_DECIMAL_DIG for the widest floating-point format supported in the implementation.]

相比之下,如果您使用 %a,并且 FLT_RADIX 是 2 的幂(意味着实现使用 floating-point 基数,即 2、16 或其他的幂2),那么C标准要求%a产生的数字扫描结果等于原数

I need the scanned float to end up being exactly, identical to the original value.

正如其他答案中已经指出的那样,可以使用 %a 格式说明符来实现。

Also, the printed value should be easily readable - to a human - as floating-point, i.e. I don't want to print 0x42355316 and reinterpret that as a 32-bit float.

这更棘手和主观。 %a 产生的字符串的第一部分实际上是由十六进制数字组成的分数,因此像 0x1.4p+3 这样的输出可能需要一些时间才能被人解析为 10 reader.

一个选项可能是打印 所有 表示浮点值所需的十进制数字,但它们可能有很多。例如,考虑值 0.1,它作为 64 位浮点数的最接近表示可能是

0x1.999999999999ap-4  ==  0.1000000000000000055511151231257827021181583404541015625

虽然 printf("%.*lf\n", DBL_DECIMAL_DIG, 01);(参见 Eric 的 )会打印

0.10000000000000001   // If DBL_DECIMAL_DIG == 17

我的提议介于两者之间。与 %a 的作用类似,我们可以将基数为 2 的任何浮点值精确表示为分数乘以 2 的某个整数次幂。我们可以将该分数转换为整数(相应地增加指数)并将其打印为十进制值。

0x1.999999999999ap-4 --> 1.999999999999a16 * 2-4  --> 1999999999999a16 * 2-56 
                     --> 720575940379279410 * 2-56

该整数的位数有限(小于 253),但结果仍然是原始 double 值的精确表示。

以下代码段是概念验证,没有对极端情况进行任何检查。格式说明符 %ap 字符分隔尾数和指数(如“...乘以 2 的 次幂 ... "),我将使用 q 代替,除了使用不同的符号外没有特别的原因。

尾数的值也将减少(指数相应增加),删除所有尾随零位。 5q+1(解析为 510 * 21)的想法应该更“容易”识别为 10,而不是 2814749767106560q-48.

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

void to_my_format(double x, char *str)
{
    int exponent;
    double mantissa = frexp(x, &exponent);
    long long m = 0;
    if ( mantissa ) {
        exponent -= 52;
        m = (long long)scalbn(mantissa, 52);
        // A reduced mantissa should be more readable
        while (m  &&  m % 2 == 0) {
            ++exponent;
            m /= 2;
        }
    }
    sprintf(str, "%lldq%+d", m, exponent);
    //                ^
    // Here 'q' is used to separate the mantissa from the exponent  
}

double from_my_format(char const *str)
{
    char *end;
    long long mantissa = strtoll(str, &end, 10);
    long exponent = strtol(str + (end - str + 1), &end, 10);
    return scalbn(mantissa, exponent);
}

int main(void)
{
    double tests[] = { 1, 0.5, 2, 10, -256, acos(-1), 1000000, 0.1, 0.125 };
    size_t n = (sizeof tests) / (sizeof *tests);
    
    char num[32];
    for ( size_t i = 0; i < n; ++i ) {
        to_my_format(tests[i], num);
        double x = from_my_format(num);
        printf("%22s%22a ", num, tests[i]);
        if ( tests[i] != x )
            printf(" *** %22a *** Round-trip failed\n", x);
        else
            printf("%58.55g\n", x);
    }
    return 0;
}

可测试here

总的来说,可读性的提高对于none来说是微乎其微的,当然见仁见智了。