【问题标题】:Why do we need std::numeric_limits::max_digits10?为什么我们需要 std::numeric_limits::max_digits10?
【发布时间】:2021-12-12 06:59:29
【问题描述】:

我了解浮点数在内存中使用符号、指数和尾数形式表示,这些形式表示每个部分的位数有限,因此会导致舍入错误。 本质上,假设我有一个浮点数,那么由于某些位数,它基本上会使用舍入策略映射到最接近的可表示形式之一。

这是否意味着 2 个不同的浮点可以映射到相同的内存表示?如果是,那么我该如何以编程方式避免它?

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

它表示浮点数中从浮点数到文本到浮点数的往返往返所需的最小位数。

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

谁能解释一下上面的场景?

【问题讨论】:

  • 请不要像以前那样缩进段落。它使它们成为代码块,将整个段落放在一行中并让人们滚动阅读。
  • "它表示浮点数在从浮点数到文本到浮点数的往返往返中所需的最小位数。"嗯,这是什么意思?跨度>
  • 当您将浮动转换为文本并再次转换回浮动时,您需要它。我不确定你到底不明白什么?
  • “在我编写的 c++ 程序中,这种往返发生在哪里。” -- 这取决于您编写的程序。它不会在所有程序中发生在同一个地方,并且有些程序根本不会发生(就像几乎其他任何事情一样,除了少数例外)。 不知道你想问什么,但也许更像是如何你可以让这个往返发生?
  • "在这个用例中我什么时候需要 std::numeric_limits::max_digits10?" -- 你已经构建了一个情境,期望如果max_digits10 是永远有用,它必须在您想出的情况下有用。这不是找到某物用途的好方法。也许你会幸运地进入一个有效的用例,但更有可能不会。通过将这些仓促的假设放入您的问题中,您有点限制了您可以获得什么样的回答。

标签: c++ floating-point precision


【解决方案1】:

暂时忘记确切的表示,假装你有一个两位浮点数。第 0 位是 1/2,第 1 位是 1/4。假设您想将此数字转换为字符串,以便在解析字符串时生成原始数字。

您可能的数字是 0、1/4、1/2、3/4。显然,您可以用小数点后的两位数来表示所有这些,并得到相同的数字,因为在这种情况下表示是准确的。但是你能逃脱一个数字吗?

假设 half 总是向上取整,则数字映射为 0、0.3、0.5、0.8。第一个和第三个数字是准确的,而第二个和第四个不是。那么当您尝试将它们解析回来时会发生什么?

0.3 - 0.25 < 0.5 - 0.30.8 - 0.75 < 1 - 0.8。很明显,在这两种情况下,四舍五入都有效。这意味着您只需要小数点后一位数字即可捕获我们设计的两位浮点数的值。

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

【讨论】:

    【解决方案2】:

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

    要知道有多少有效小数位将浮点类型转换为该类型所有可能值的独特文本。


    这是否意味着 2 个不同的浮点可以映射到相同的内存表示?如果是,那么我该如何以编程方式避免它?

    不,不同的浮点对象不同,会有不同的编码。

    是的,文本不同的不同浮点代码可能映射到相同的内存表示。 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_digits10x == y 对于某些x 可能仍然正确,但不是全部。

    如果有超过max_digits10 的有效十进制数字,x == y 对所有x 都是正确的,但s 会不必要地增长。


    有效的十进制数字

    有效数字开始计数不是. 右侧的位数,而是从最高有效非零数字开始的计数。以下所有内容,作为代码或文本,都有 9 位有效的十进制数字。

    1.23456789
    12345.6789
    123456789.
    123456789f
    1.23456789e10
    1.23456789e-10
    -1.23456789
    12345.0000
    00012345.6789
    

    【讨论】:

    • 我在 C++ 措辞中担心的一件事是,可能有一个 x 和一个 y 映射到不同的十进制字符串,但是将它们转换回来会为两者生成 x。例如,假设连续的浮点值是 9.821...、9.942... 和 10.063... 舍入到小数点后两位,后两位分别为 9.9 和 10。然后转换回来得到 9.94(三者中最接近 9.9)和 9.94(三者中最接近 10)。如果是这样,那么 C++ 的“始终有区别”不同于 C 和 IEEE-754 的“往返工作”。 …
    • ... 在上面的例子中,如果 10.184 跟在后面,它会破坏这个例子,因为 10.063 和 10.184 不会映射到有区别的两位数(都到 10),所以 2 不可能是 @ 987654341@ 这种格式。但也许这发生在指数范围的边缘,所以 10.184 不在可表示的数字集中。所以需要进一步研究。如果能发生,我认为是C++标准的缺陷;这可能不仅仅是为了产生差异化,而是为了完全确保往返行程和最近的工作。
    • @EricPostpischil 您是否建议max_digits10 == 2 用于这些评论示例? (在这种情况下,FP 类型的有效位最多有 3 个二进制数字。)
    • 是的,但这只是为了说明。在实际格式中,是否有一些 x 和 y 转换为不同的 16 位十进制数字,它们都转换回 x? (这实际上不会使max_digits10 16,因为double 格式中有许多其他数字需要区分17 位数字。所以它不会导致C++ 对max_digits10 的定义具有错误的值。问题是否存在任何浮点格式和一个 n 使得它的所有值都转换为不同的 n 十进制数字但有些不转换回它们的原始值?)
    • @EricPostpischil 我没有找到“x 和 y 转换为不同的 16 位十进制数字并都转换回 x”的示例,所以在这一点上假设它是正确的。会考虑更小的n
    【解决方案3】:

    您似乎将舍入(和精度损失)的两个来源与浮点数混淆了。

    浮点表示

    第一个是由于浮点数在内存中表示的方式,正如您刚才指出的那样,它使用二进制数作为尾数和指数。经典的例子是:

    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 或写入文件时。 p>

    当您使用%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 将这两个浮点值作为文本保存到文件中,并使用默认位数,您可能已经将相同的值保存了两次,而原来的 @ 有两个不同的值987654335@和y

    max_digits10 是“我需要写多少个十进制数字才能避免所有可能值出现这种情况?”这个问题的答案。换句话说,如果您用max_digits10 数字(恰好是浮点数为 9)编写浮点数并将其加载回来,则保证得到与开始时相同的值。

    请注意,写入的十进制值可能与浮点数的实际值不同(由于表示不同。但仍然保证比您将十进制数的文本读取 float 你会得到相同的值。

    编辑:一个例子

    查看那里的代码不足:https://ideone.com/pRTMZM

    假设你有之前的两个floats,

    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,如果我们想确保不同的浮动可以使文本往返并保持不同。

    【讨论】:

    • 嗯,只是一个评论。为什么要使用 printf 来回答 C++ 函数。也许 iostream 函数会更合适。但无论如何。你很好地回答了问题的本质。谢谢+1
    • @Louen - 感谢您的精彩解释。您能否详细说明您的最后一段,以便我可以通过测试程序看到它?
    • 请注意,.1 和 .2 也不能完美表示。
    • @Test 这是你的例子! @ArminMontigny:在这种情况下,printf 通过在格式字符串中显式设置精度对我来说更简洁。在我编辑的答案中,我还使用了setprecision
    • 为避免double rounding 分心,最好附加f,如float x = 10.0000095; --> float x = 10.0000095f; 用于初始化float
    【解决方案4】:

    在我编写的 c++ 程序中,这种往返发生在哪里。

    这取决于您编写的代码,但一个明显的地方是......您在代码中放入的任何浮点 文字

    float f = 10.34529848505433;
    

    f 会是那个数字吗?不,这将是该数字的近似值,因为float 的大多数实现都无法存储那么高的精度。如果您将文字更改为10.34529848505432,则很有可能f 将具有相同的值。

    这与往返本身无关。 standard defines max_digits10 purely in terms of going from decimal to float

    确保始终区分不同值所需的以 10 为基数的位数。

    【讨论】:

    • 解析十进制文字以初始化 float 不是往返;这是一个单一的方向。即使将其转换回十进制并打印,那也是从十进制到float 到十进制的往返,这不是问题中所问的往返,从float 到十进制到float。那次往返与max_digits10 相关; max_digits10 是中间小数所需的位数,以确保往返返回到原始 float
    • @EricPostpischil: "而且往返与 max_digits10 相关;" Not according to the standard: "确保不同的值始终存在所需的以 10 为基数的位数差异化。”我不知道人们从哪里得到这种“往返”的东西,但它不是来自 C++ 标准。
    • 考虑“确保始终区分不同的值”的含义。这意味着如果xy 是不同的浮点值,那么将它们转换为带有max_digits10 有效数字的十进制可以确保它们是有区别的(转换结果对于x 和@987654340 会产生不同的结果@),这意味着在转换回来时可以确定原始值。如果它们没有区别,那将是不可能的。因此,该措辞与往返旅行重现原始价值的含义相同,只是用词不同。
    • “往返”措辞是数学家用来说明十进制数字包含足以区分原始值的信息的方式。这两个语句在数学上是等价的,关键是向用户提供保证提供足够信息所需的有效数字的最小数量。 C++ 的 max_digits10 来自 C 的 FLT_DECIMAL_DIGDBL_DECIMAL_DIGLDBL_DECIMAL_DIG,它们是用往返语句定义的 (C 2018 5.2.4.2.2 12)。
    • 而这又源于 IEEE 754,在 2008 版本中,它具有对等值 Pmin ( bf ) 的描述(Pmin 类似于max_digits10*bf* 是格式,类似于floatdouble)出现在5.12.2 中,上面写着“Conversions from a supported binary format bf 到外部字符序列并再次返回会导致原始数字的副本,只要至少指定了 Pmin ( bf ) 有效数字并且在两次转换期间生效的舍入方向属性是舍入到最接近的舍入方向属性。”
    猜你喜欢
    • 2016-03-14
    • 2019-04-26
    • 1970-01-01
    • 2019-06-09
    • 2021-11-11
    • 2011-03-16
    • 1970-01-01
    • 2014-06-18
    • 2017-02-26
    相关资源
    最近更新 更多