如何以完美的精度打印浮点值以供以后扫描?
How do I print a floating-point value for later scanning with perfect accuracy?
假设我有一个 float
或 double
类型的浮点值(即典型机器上的 32 或 64 位)。我想将这个值打印为文本(例如到标准输出流),然后在其他一些进程中将其扫描回来 - 如果我使用 C,则使用 fscanf()
,或者可能使用 istream::operator>>()
如果我使用 C++。但是 - 我需要扫描的浮点数最终成为 exactly,与原始值相同(直到相同值的等效表示)。此外,打印的值应该易于阅读 - 对于人类来说 - 作为浮点数,即我不想打印 0x42355316 并将其重新解释为 32 位浮点数。
我应该怎么做?我假设(C 和 C++)的标准库是不够的,但也许我错了。我想足够多的小数位数可能能够保证低于精度阈值的错误 - 但这与保证 rounding/truncation 会按照我想要的方式发生是不一样的。
备注:
- 扫描不必非常准确w.r.t。它扫描的值,只有原始值。
- 如果这样更简单,您可以假设该值是一个数字而不是无穷大。
- 需要非规范化支持,但不是必需的;尽管如此,如果我们得到一个反常态,失败应该是显而易见的。
您可以使用 %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
格式与 fprintf
和 fscanf
一起使用。这就是它的设计目的,如果实现使用二进制 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
,其中 Type
是 float
或 double
或另一个 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
值的精确表示。
以下代码段是概念验证,没有对极端情况进行任何检查。格式说明符 %a
用 p
字符分隔尾数和指数(如“...乘以 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来说是微乎其微的,当然见仁见智了。
假设我有一个 float
或 double
类型的浮点值(即典型机器上的 32 或 64 位)。我想将这个值打印为文本(例如到标准输出流),然后在其他一些进程中将其扫描回来 - 如果我使用 C,则使用 fscanf()
,或者可能使用 istream::operator>>()
如果我使用 C++。但是 - 我需要扫描的浮点数最终成为 exactly,与原始值相同(直到相同值的等效表示)。此外,打印的值应该易于阅读 - 对于人类来说 - 作为浮点数,即我不想打印 0x42355316 并将其重新解释为 32 位浮点数。
我应该怎么做?我假设(C 和 C++)的标准库是不够的,但也许我错了。我想足够多的小数位数可能能够保证低于精度阈值的错误 - 但这与保证 rounding/truncation 会按照我想要的方式发生是不一样的。
备注:
- 扫描不必非常准确w.r.t。它扫描的值,只有原始值。
- 如果这样更简单,您可以假设该值是一个数字而不是无穷大。
- 需要非规范化支持,但不是必需的;尽管如此,如果我们得到一个反常态,失败应该是显而易见的。
您可以使用 %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
格式与 fprintf
和 fscanf
一起使用。这就是它的设计目的,如果实现使用二进制 floating-point.
否则,您应该打印一个至少有 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
,其中 Type
是 float
或 double
或另一个 floating-point类型。
请注意,C 标准仅建议这样的 round-trip 通过十进制数字工作;它不需要它。例如,C 2018 5.2.4.2.2 15 在“推荐做法”标题下说:
Conversion from (at least)
double
to decimal withDECIMAL_DIG
digits and back should be the identity function. [DECIMAL_DIG
is the equivalent ofFLT_DECIMAL_DIG
orDBL_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
值的精确表示。
以下代码段是概念验证,没有对极端情况进行任何检查。格式说明符 %a
用 p
字符分隔尾数和指数(如“...乘以 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来说是微乎其微的,当然见仁见智了。