【问题标题】:Why does "dtoa.c" contain so much code?为什么“dtoa.c”包含这么多代码?
【发布时间】:2010-07-03 22:30:31
【问题描述】:

我将是第一个承认我对低级编程的整体知识有点稀疏的人。我了解许多核心概念,但我不经常使用它们。 话虽如此,我对dtoa.c 需要多少代码感到非常震惊。

在过去的几个月里,我一直致力于 C# 中的 ECMAScript 实现,我一直在减慢填补引擎漏洞的速度。昨晚我开始研究 Number.prototype.toString,这在 ECMAScript specification(pdf)15.7.4.2 部分有所描述。在 9.8.1 部分,NOTE 3 提供了一个到 dtoa.c 的链接,但我正在寻找一个挑战,所以我等待查看它。以下是我想出的。

private IDynamic ToString(Engine engine, Args args)
{
    var thisBinding = engine.Context.ThisBinding;
    if (!(thisBinding is NumberObject) && !(thisBinding is NumberPrimitive))
    {
        throw RuntimeError.TypeError("The current 'this' must be a number or a number object.");
    }

    var num = thisBinding.ToNumberPrimitive();

    if (double.IsNaN(num))
    {
        return new StringPrimitive("NaN");
    }
    else if (double.IsPositiveInfinity(num))
    {
        return new StringPrimitive("Infinity");
    }
    else if (double.IsNegativeInfinity(num))
    {
        return new StringPrimitive("-Infinity");
    }

    var radix = !args[0].IsUndefined ? args[0].ToNumberPrimitive().Value : 10D;

    if (radix < 2D || radix > 36D)
    {
        throw RuntimeError.RangeError("The parameter [radix] must be between 2 and 36.");
    }
    else if (radix == 10D)
    {
        return num.ToStringPrimitive();
    }

    var sb = new StringBuilder();
    var isNegative = false;

    if (num < 0D)
    {
        isNegative = true;
        num = -num;
    }

    var integralPart = Math.Truncate(num);
    var decimalPart = (double)((decimal)num.Value - (decimal)integralPart);
    var radixChars = RadixMap.GetArray((int)radix);

    if (integralPart == 0D)
    {
        sb.Append('0');
    }
    else
    {
        var integralTemp = integralPart;
        while (integralTemp > 0)
        {
            sb.Append(radixChars[(int)(integralTemp % radix)]);
            integralTemp = Math.Truncate(integralTemp / radix);
        }
    }

    var count = sb.Length - 1;
    for (int i = 0; i < count; i++)
    {
        var k = count - i;
        var swap = sb[i];
        sb[i] = sb[k];
        sb[k] = swap;
    }

    if (isNegative)
    {
        sb.Insert(0, '-');
    }

    if (decimalPart == 0D)
    {
        return new StringPrimitive(sb.ToString());
    }

    var runningValue = 0D;
    var decimalIndex = 1D;
    var decimalTemp = decimalPart;

    sb.Append('.');
    while (decimalIndex < 100 && decimalPart - runningValue > 1.0e-50)
    {
        var result = decimalTemp * radix;
        var integralResult = Math.Truncate(result);
        runningValue += integralResult / Math.Pow(radix, decimalIndex++);
        decimalTemp = result - integralResult;
        sb.Append(radixChars[(int)integralResult]);
    }

    return new StringPrimitive(sb.ToString());
}

谁能解释一下为什么 dtoa.c 有大约 40 倍的代码?我简直无法想象 C# 会变得如此高效。

【问题讨论】:

  • 你怎么想不到呢?这种差异正是 C# 等语言如此受欢迎的原因。
  • @Neil - 我想这并不难想象。我应该已经意识到让原生代码跨平台需要做多少工作。
  • 你用负输入测试过这段代码吗?
  • @Mark - 我会被诅咒的,有一个负数的错误。不错的收获。
  • 我还怀疑(1.3333333333333333).toString(3) 没有给1.1 你的实现...

标签: c# javascript c floating-point ecma262


【解决方案1】:

dtoa.c 包含两个主要函数:dtoa(),它将双精度数转换为字符串,以及 strtod(),它将字符串转换为双精度数。它还包含很多支持函数,其中大部分是针对它自己的任意精度算术实现的。 dtoa.c 声名鹊起的是正确地进行这些转换,而这通常只能通过任意精度的算术来完成。它还具有在四种不同舍入模式下正确舍入转换的代码。

您的代码仅尝试实现 dtoa() 的等效项,并且由于它使用浮点进行转换,因此并不总是正确。 (更新:详见我的文章http://www.exploringbinary.com/quick-and-dirty-floating-point-to-decimal-conversion/。)

(我在我的博客http://www.exploringbinary.com/ 上写了很多关于此的内容。我过去的七篇文章中有六篇仅关于 strtod() 转换。通读它们以了解正确舍入转换是多么复杂。 )

【讨论】:

  • 非常有趣,我有一些阅读要做,我肯定需要更详细地探索这段代码。
  • 确实不难相信我会错过这个文件中看到的“双向”转换。代码对我来说真的很难阅读。
  • +1。更不用说 Gay 的dtoa(它是 dtoa.c 中两个功能中较小的一个)还允许用户指定输出的位数,以及使用哪种舍入方法。坦率地说,问题中的 C# 代码只完成了 dtoa.c 的一小部分,并且执行起来不准确、缓慢且错误(例如,尝试使用负数)。
  • @Chaos:你这个问题的依据基本上是; “我的代码完成了 dtoa 所做的一切,为什么 dtoa 这么大?”这是不正确的,你的代码根本不像 dtoa 那样做,所以当人们打电话给你时不要感到惊讶。
  • 哇,这里的对话太多了。你们应该在聊天中做到这一点。
【解决方案2】:

为十进制和二进制浮点表示之间的转换产生良好的结果是一个相当困难的问题。

困难的主要来源是许多小数,即使是简单的小数,也不能准确用二进制浮点表示——例如,0.5 可以(显然),但0.1不能。并且,反过来(从二进制到十进制),您通常不想要绝对准确的结果(例如,最接近 0.1 的数字的准确十进制值,可以在符合 IEEE-754 的double 其实是 0.1000000000000000055511151231257827021181583404541015625) 所以你通常需要一些舍入。

因此,转换通常涉及近似值。良好的转换例程保证在特定(字长或位数)约束内产生最接近可能的近似值。这就是大部分复杂性的来源。

查看dtoa.c 实现顶部的评论中引用的论文,Clinger 的How to Read Floating Point Numbers Accurately,以了解问题的含义;或许还有 David M. Gay(作者)的论文,Correctly Rounded Binary-Decimal and Decimal-Binary Conversions

(另外,更一般地说:What Every Computer Scientist Should Know About Floating Point Arithmetic。)

【讨论】:

  • 我很清楚浮点所代表的挑战。我的问题更多地与代码量的差异有关。当我针对 Firefox 测试我的示例时,我的代码产生了相同的结果。 Firefox 确实使用 dtoa.c.
【解决方案3】:

快速浏览一下,相当多的 C 版本正在处理多个平台,并且看起来该文件旨在通用地跨编译器(C 和 C++)、位数、浮点实现使用和平台;具有大量 #define 可配置性。

【讨论】:

  • 我同意。我还看了一眼代码,我厌倦了它为如此简单的操作所代表的工作。对于 .NET,我幸运地看到了应用程序编程的时代已经过去。
  • 拥有一个 c 文件的存档不是更容易,每个文件都支持一个单独的平台吗?阅读所有这些 #ifdef 块让我患上了脑动脉瘤……
  • 是的,问题不在于语言,而在于整体设计笨拙。更好的模块化也可以达到同样的效果。
  • @ChaosPandion:取决于您是希望在 C 头文件(设置定义)还是在 makefile(选择要构建的正确 .c 文件)中完成特定于平台的配置。此外,如果有足够的定义给你一个动脉瘤,如果所有的可能性都相乘,那可能是 2^aneurysm 不同的 c 文件。实际上,在编写这种超便携的东西时,您通常需要由大量配置选项驱动的少量源文件(在本例中为 1,但有时更多),而不是相反。
  • ... 您可能希望根据平台的行为/属性编写代码以支持平台,而不是为每个平台只编写一个 dtoa 实现,以便您可以移植到新的平台只需编写具有正确定义组合的头文件,而不是每次都重写所有 stdlib。像这样的大规模可配置实现并不总是最好的,但当您考虑替代方案时,它可能并不像看起来那么糟糕。
【解决方案4】:

我还认为 dtoa.c 中的代码可能更有效(独立于语言)。例如,它似乎在做一些琐碎的事情,这在专家手中通常意味着速度。出于速度原因,我认为它只是使用了一种不太直观的算法。

【讨论】:

  • 很多C库函数(和C库扩展)都是这样写的。这是有道理的,因为它们通常不会非常频繁地更新、编辑或以其他方式维护,但是它们会从大量程序中到处调用。即使以牺牲一些可维护性为代价,也要尽可能快地制造它们才有意义。
【解决方案5】:

简答:因为dtoa.c 有效。

这正是调试良好的产品和 NIH 原型之间的区别。

【讨论】:

  • “此处未发明”,如“自定义 ad-hok 代码,作为流行的 3rd 方代码的替代品”
  • 我的意思是你是对的,但它没有解释为什么有这么多代码。谁知道也许其他人编写了一个正确转换的实现,其代码减少了一半,功能增加了两倍。
猜你喜欢
  • 1970-01-01
  • 2016-09-23
  • 2015-04-04
  • 1970-01-01
  • 2023-04-05
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 2012-05-14
相关资源
最近更新 更多