我们为什么需要std::numeric_limits::max_digits10?

4

我了解到浮点数在内存中使用符号、指数和尾数的形式表示,每个部分都有限定数量的位来表示,因此会出现舍入误差。实际上,假设我有一个浮点数,由于一定数量的位数,它基本上会通过四舍五入策略映射到最近的可表示形式之一。

这是否意味着两个不同的浮点数可以映射到相同的内存表示?如果是,那么我如何通过编程避免这种情况?

我看到了这个std::numeric_limits<T>::max_digits10

它表示一个浮点数要经过从浮点数到文本再到浮点数的往返操作所需的最小位数。

在我编写的 C++ 程序中,这个往返操作会在哪里发生?据我所知,我有一个存储在内存中(可能带有舍入误差)的浮点数 f1,并被读取回来。我可以直接在 C++ 程序中有另一个浮点变量 f2,然后可以将其与原始浮点 f1 进行比较。现在我的问题是,在这种用例中何时需要使用 std::numeric_limits::max_digits10?是否有任何用例可以说明我需要使用 std::numeric_limits::max_digits10 以确保我不会做错事?

有人能解释一下以上场景吗?


2
请不要像您所做的那样缩进段落。这会将它们变成代码块,使整个段落放在一行上,并迫使人们滚动阅读。 - Nate Eldredge
1
“它”指的是浮点数在从浮点数转换为文本再转换回浮点数时所需的最小位数。 - Nicol Bolas
2
当您将浮点数转换为文本,然后再次转换为浮点数时,您需要它。我不确定您到底不理解什么? - Alan Birtles
1
“在我编写的C++程序中,这个往返发生在哪里?”-- 这取决于您编写的程序。它不会在所有程序中的同一位置发生,并且有些程序根本不会发生(就像几乎任何其他事情一样,有少数例外)。不确定您想问什么,但也许更像是“如何”才能使这个往返发生? - JaMiT
2
“在这个使用情景中什么时候需要std::numeric_limits::max_digits10?”——你构建了一个期望如果max_digits10有用,那它必须在你想出的情况下有用的局面。这不是发现某物的好方法。也许你会有运气找到一个有效的用例,但更可能不会。通过将这些草率的假设投入你的问题中,你限制了可以得到的响应类型。 - JaMiT
4个回答

3

为什么需要使用std::numeric_limits::max_digits10?

为了知道将浮点数类型转换为文本形式所需的最大有效小数位数,以确保对该类型的所有可能值都具有唯一性。


这是否意味着两个不同的浮点数可以映射到相同的内存表示?如果是的话,我如何在程序中避免这种情况?

不会,不同的浮点数对象,其不同,将具有不同的编码。

是的,不同的浮点代码,它们文本不同,可能映射到相同的内存表示。下面的x1,x2肯定具有相同的编码。32位float只能编码约232个不同的值。许多不同的浮点常量映射到相同的float

float x1 = 1.000000000000000001f;
float x2 = 1.000000000000000001000000000000000001f;
assert(x1 == x2);

在我编写的 C++ 程序中,这个往返发生在哪里?现在我的问题是在什么情况下需要使用 std::numeric_limits::max_digits10?是否有任何用例可以说明我需要使用 std::numeric_limits::max_digits10 以确保我不会做错事。
如果代码将浮点数 x 转换为字符串 s ,然后再将其转换回浮点数 y ,那么这就是我们关注的"往返"问题。
为了使 x == y 成立,s 中应该至少包含 max_digits10 个有效十进制数字,以适用于所有的 x。
如果有效十进制数字少于 max_digits10,则对于某些 x,x == y 仍然成立,但并非所有情况。
如果有效十进制数字多于 max_digits10,则对于所有 x,x == y 都成立,但 s 会变得不必要地长。
有效的十进制位数计数不是指 "." 右侧的数字数量,而是指从最高有效的非零数字开始的数量。以下所有示例都具有 9 个有效十进制数字。
1.23456789
12345.6789
123456789.
123456789f
1.23456789e10
1.23456789e-10
-1.23456789
12345.0000
00012345.6789

1
我在C++语言中担心的一件事是,可能存在一个x和一个y映射到不同的十进制字符串,但将它们转换回来会得到相同的x。例如,假设连续的浮点值为9.821…、9.942…和10.063…。舍入为两个小数位后,后两个值变成了9.9和10。然后将其转换回来会得到9.94(最接近9.9的三个值之一)和9.94(最接近10的三个值之一)。如果是这样,那么C++的“始终区分”与C和IEEE-754的“往返工作”不同。... - Eric Postpischil
1
通过上面的例子,如果接下来出现10.184,它将破坏这个例子,因为10.063和10.184都不能映射到不同的两位数(都变成了10),所以2不能是这种格式的max_digits10。但也许这只发生在指数范围的边缘,因此10.184不在可表示数字的集合中。因此需要进一步研究。如果它确实发生了,我认为这是C++标准中的一个缺陷;它很可能不仅仅是为了产生差异,而是要完全确保舍入到最近的工作的往返。 - Eric Postpischil
1
@EricPostpischil 您是否建议在这些注释示例中使用 max_digits10 == 2?(这种情况下,FP类型在尾数中最多有3个二进制数字。) - chux - Reinstate Monica
1
是的,但那只是为了举例说明。在实际格式中,是否存在一些x和y,它们转换为不同的16位十进制数字,但都可以转换回x?(这实际上不会使max_digits10等于16,因为在double格式中还有许多其他需要17位数字才能区分的数字。因此,它不会导致C++对max_digits10的定义具有错误的值。问题是是否存在任何浮点格式和n,使其所有值都转换为不同的n位十进制数字,但某些值不能转换回它们的原始值?) - Eric Postpischil
1
@EricPostpischil 我没有找到“x和y的例子,它们转换为不同的16位十进制数字,两者都可以转换回x”,所以现在假设它是真的。将考虑更小的n - chux - Reinstate Monica
@EricPostpischil 仍然没有找到反例 - 假设我理解了这个问题。尝试找到往返所需的最小十进制位数是有趣的。如果我没记错,MS VS C++ 有一个格式说明符来做到这一点,但对某些值得到了错误的函数。 - chux - Reinstate Monica

3

先不考虑精确表示法,假装你有一个两位的浮点数。第0位是1/2,第1位是1/4。假设你想把这个数字转换成一个字符串,使得当该字符串被解析时,它会生成原始数字。

你可能的数字是0、1/4、1/2、3/4。显然,你可以用小数点后两位来表示它们并获得相同的数字,因为在这种情况下表示是精确的。但是,你是否可以只使用一个数字呢?

假设一半总是四舍五入,那么这些数字将映射到0、0.3、0.5、0.8。第一个和第三个数字是精确的,而第二个和第四个数字不是。那么当你尝试重新解析它们时会发生什么?

0.3 - 0.25 < 0.5 - 0.3,且 0.8 - 0.75 < 1 - 0.8。所以,在这两种情况下,四舍五入都管用。这意味着你只需要小数点后一位数字就可以捕捉到我们人为构造的两位浮点数的值。

你可以将位数从两位扩展到53位(对于double),并添加一个指数来改变数字的比例,但是概念是完全相同的。


2
你似乎混淆了浮点数的两种舍入(和精度损失)来源。
浮点表示
第一种是由于浮点数在内存中的表示方式,它使用二进制数来表示尾数和指数,就像你刚指出的那样。经典的例子是:
const float a = 0.1f;
const float b = 0.2f;
const float c = a+b;

printf("%.8f + %.8f = %.8f\n",a,b,c);

这将打印输出

0.10000000 + 0.20000000 = 0.30000001

在这里,数学上正确的结果是0.3,但是0.3无法用二进制表示。相反,你会得到最接近的可以表示的数字。

保存为文本

另一个场景,也就是max_digits10发挥作用的地方,是浮点数的文本表示,例如使用printf或写入文件时。

当你使用%f格式说明符打印数字时,你将得到以十进制打印的数字。

在以十进制方式打印数字时,你可以决定打印出多少位小数。在某些情况下,你可能无法精确地打印出实际数字。

例如,考虑以下内容:

const float x = 10.0000095f;
const float y = 10.0000105f;
printf("x = %f ; y = %f\n", x,y);

这将会打印出来

x = 10.000010 ; y = 10.000010

另一方面,使用%.8fprintf的精度提高到8位小数会给你。

 x = 10.00000954 ; y = 10.00001049

如果您想使用fprintfofstream将这两个浮点值保存为文本文件,并且使用默认小数位数,则可能会出现保存相同值两次的情况,即使原来的xy有两个不同的值。

max_digits10是回答“我需要写多少位小数才能避免所有可能的值?”的答案。换句话说,如果您使用max_digits10位数(对于浮点数而言,这恰好是9)编写您的浮点数并将其加载回来,则保证获得与开始时相同的值。

请注意,所写的十进制值可能与浮点数的实际值不同(由于不同的表示)。但是,当您将十进制数的文本读入float时,您将获得相同的值。

编辑:一个例子

查看代码运行结果:https://ideone.com/pRTMZM

假设您有之前提到的两个float

const float x = 10.0000095f;
const float y = 10.0000105f;

如果你想将它们保存为文本(一个典型的用例是保存到可读性格式,如XML或JSON,甚至使用打印进行调试),那么你需要进行翻译。在我的示例中,我将使用stringstream将其写入字符串。

首先让我们尝试默认精度:

stringstream def_prec;
def_prec << x <<" "<<y;

// What was written ?
cout <<def_prec.str()<<endl;

在这种情况下的默认行为是在写入文本时将我们的每个数字四舍五入为10。因此,如果我们使用该字符串读取回另外两个浮点数,它们将不会包含原始值:
float x2, y2;
def_prec>>x2 >>y2;

// Check
printf("%.8f vs %.8f\n", x, x2);
printf("%.8f vs %.8f\n", y, y2);

同时会打印出以下结果

10 10
10.00000954 vs 10.00000000
10.00001049 vs 10.00000000

这个从浮点数到文本再返回的过程中丢失了很多可能是重要的数字。显然,我们需要以更高的精度将值保存为文本。文档保证使用 max_digits10 不会在这个过程中丢失数据。让我们试试使用 setprecision

const int digits_max = numeric_limits<float>::max_digits10;
stringstream max_prec;
max_prec << setprecision(digits_max) << x <<" "<<y;
cout <<max_prec.str()<<endl;

现在将会打印出来

10.0000095 10.0000105

所以这次我们的值保存了更多的数字。让我们尝试读取回来:
float x2, y2;
max_prec>>x2 >>y2;
    
printf("%.8f vs %.8f\n", x, x2);
printf("%.8f vs %.8f\n", y, y2);

打印哪一个

10.00000954 vs 10.00000954
10.00001049 vs 10.00001049

啊哈!我们找回了我们的数值!

最后,让我们看看如果我们使用比max_digits10少一个数字会发生什么。

stringstream some_prec;
some_prec << setprecision(digits_max-1) << x <<" "<<y;
cout <<some_prec.str()<<endl;

这是我们以文本形式保存的内容。
10.00001 10.00001

并且我们读取回来:

10.00000954 vs 10.00000954
10.00001049 vs 10.00000954

因此,在这里,精度足以保持x的值,但不能保留y的值,因为它被向下舍入。这意味着我们需要使用max_digits10,如果想确保不同的浮点数可以进行往返转换并保持不同。


2
嗯,只是一条评论。为什么要使用printf来回答C++函数呢?也许iostream函数会更合适。但无论如何,你非常好地回答了问题的实质。谢谢+1。 - A M
1
@Louen - 感谢您的出色解释。您能否详细说明一下您最后一段话,以便我可以通过测试程序看到它? - Test
2
请注意,.1和.2也无法完美地表示。 - Yakk - Adam Nevraumont
1
@Test 这是你的例子! @ArminMontigny:在这种情况下,对我来说,使用 printf 更加简洁明了,因为可以在格式字符串中显式设置精度。在我编辑后的答案中,我也使用了 setprecision - Louen
2
为避免双重舍入的干扰,最好在初始化float时附加一个f,例如float x = 10.0000095; --> float x = 10.0000095f; - chux - Reinstate Monica
printf("%.8f vs %.8f\n", x, x2); 是一个糟糕的例子,因为所需的数字位数与 固定 点无关,而是与 指数 表示法有关。建议使用 printf("%.8e vs %.8e\n", x, x2);。类似的问题也适用于各种 C++ 输出 - 实际上,大多数都适用于这个答案。 - chux - Reinstate Monica

1
这取决于你编写的代码,但一个显而易见的地方是... 你在代码中放置任何浮点字面量的位置:
float f = 10.34529848505433;

“f”会精确地等于那个数吗?不会。因为大多数“float”的实现无法存储那么多的精度,所以它将是该数字的近似值。如果您将文字更改为“10.34529848505432”,那么很有可能“f”的值将相同。
这不是关于往返转换的问题。标准纯粹从十进制到浮点数定义了“max_digits10”: “确保区分差异值所需的基本10位数字的数量。”

1
将十进制文字面量解析为float以初始化不是一个往返过程;它是单向的。即使将其转换回十进制并打印,也是从十进制到float再到十进制的往返,这不是问题中所询问的从float到十进制再到float的往返。而那个往返与max_digits10有关;max_digits10是需要保证该中间十进制位数的数字,以确保往返返回到原始的float - Eric Postpischil
1
@EricPostpischil:“而这个往返与max_digits10有关;根据标准并非如此:需要的十进制位数,以确保不同的值始终被区分。”我不知道人们从哪里得到这个“往返”的东西,但它不是来自C++标准。 - Nicol Bolas
2
考虑“确保不同的值始终有所区别”的含义。这意味着,如果 xy 是不同的浮点数值,则使用 max_digits10 个有效数字将它们转换为十进制可以确保它们被区分(转换的结果对于 xy 产生不同的结果),这意味着在转换回来时可以确定原始值。如果它们没有被区分,那就不可能了。因此,这种措辞与往返旅行再现原始值的含义相同,只是用不同的词语表达而已。 - Eric Postpischil
2
“Round trip”这个短语是数学家表达十进制数包含足够信息以区分原始值的方式。这两个陈述在数学上是等价的,重点是向用户传递所需的最少有效数字,以确保提供足够的信息。C++的max_digits10来自于C的FLT_DECIMAL_DIGDBL_DECIMAL_DIGLDBL_DECIMAL_DIG,这些都是用“round-trip”短语定义的(C 2018 5.2.4.2.2 12)。 - Eric Postpischil
2
这源于IEEE 754,而在2008年版本中,等效值的描述Pminbf)(Pmin类似于max_digits10,而*bf*是格式,类似于floatdouble)出现在5.12.2中,其中说:“从受支持的二进制格式bf到外部字符序列的转换以及再次返回会导致原始数字的副本,只要至少指定了Pminbf)个有效数字,并且在两次转换期间生效的舍入方向属性是四舍五入方向属性。” - Eric Postpischil
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接