重新实现.NET Core的 double.ToString()

目前C#中double.ToString()的问题

.NET Core 2.0发布后,整个框架的性能提升了不少。但是仍然有一些基础数据类型的性能不尽入人意,例如double.ToString()这个常用方法的性能。一开始 .NET Core团队发现这个问题,是因为double.ToString()在Linux下的性能比在Windows下慢7倍. 后来为了提升性能,.NET Core团队利用Core RT的代码重写了造成性能瓶颈的 _ecvt 函数,但性能仍然比在Windows下慢了3倍

您可以分别在Linux和Windows下运行以下示例代码,观察结果 (注,需要安装BenchMarkDoNet):

[Benchmark]
[InlineData("fr")]
[InlineData("da")]
[InlineData("ja")]
[InlineData("")]
public void ToString(string culturestring)
{
    double number = 104234.343;
    CultureInfo cultureInfo = new CultureInfo(culturestring);
    foreach (var iteration in Benchmark.Iterations)
        using (iteration.StartMeasurement())
        {
            for (int i = 0; i < innerIterations; i++)
            {
                number.ToString(cultureInfo); number.ToString(cultureInfo); number.ToString(cultureInfo);
                number.ToString(cultureInfo); number.ToString(cultureInfo); number.ToString(cultureInfo);
                number.ToString(cultureInfo); number.ToString(cultureInfo); number.ToString(cultureInfo);
            }
        }
}

为什么会造成这个问题呢?首先,对于32bit Windows, double.ToString()的实现是用纯汇编直接写的,并不会去调用_ecvt(代码看这里). 所以可以先排除32位Windows的情况。

其次,虽然64bit的Windows和Linux的double.ToString()是同一份代码,但是_ecvt的实现依赖于一个C函数snprintf. 显然Windows下的snprintf做了更多优化,性能超过Linux, 才造成了double.ToString()在Windows和Linux平台的性能差距。

实际上无论Windows还是Linux, 其性能都非常低。这是因为现在的实现方式是有问题的。现在的大致流程是:

  • 将double通过snprintf转换成字符串。
  • 对转换后的字符串做字符串操作,组装成我们期待的格式。

snprintf已经很慢了,还需要对其结果做字符串操作,使得性能进一步降低。

能否不依赖snprintf, 直接将double转换成我们期待的格式呢?其实早在90年代就已经有了论文,我就是以这个论文为基础重写的double.ToString().

为了实现论文中的算法,我们需要一些基础知识。

double数据类型在内存中是如何存储的

众所周知,浮点数是无法精确地被二进制表示的。但是原因是什么呢? 这和浮点数在内存中的存储方式有关。 IEEE提出了一个浮点数的设计规范,完整的内容可以参考Wiki.

IEEE Double-precision Floating Point存储结构

double数据类型在内存中占用64位。这64位分为三个部分,分别是sign(符号, 1bit), exponent(指数, 11bit), fraction(因子, 52bit). 如下图所示:

double-mem.png

  • 第63位为符号位,表示正负, 以sign表示。
  • 62~52位表示指数,以e表示。具体的含义可以看后面的解释。
  • 51~0位表示double的具体数值,以f表示。具体的含义可以看后面的解释。

利用这3个概念,就可以表示一个double数值了 (后面会解释为什么下面的公式中指数要减去1023):

(-1)^sign(1.f) * 2^(e - 1023)

或者将f展开,看起来更清晰:
double-f1.png

进一步展开可得:

double-f2.png

为什么指数需要减去1023呢?在IEEE的规范中,e - 1023叫做biased exponent. biased exponent的具体概念不会在这篇文章中讲解,目前您只需要记住double的指数不是直接使用e, 而是使用biased exponent (即e - 1023)就行了。

有了以上概念,大家可以试着算一下下面的二进制表示的double数值是多少:

0100000000110111000000000000000000000000000000000000000000000000

0011111111100000000000000000000000000000000000000000000000000000

附上C++查看内存数据的代码:

#include <iostream>
#include <bitset>

int main()
{
    double d = 0.5; // 修改成您想查看的数字
    unsigned long long i = *((unsigned long long*)&d);
    std::bitset<64> b(i);

    std::cout << b << std::endl;

    return 0;
}

double的精度问题

由于计算机只能用二进制表示double, 所以有一些限制。例如0.5和0.25是很容易表示的(可以试着写一下这两个数的内存数据). 但是0.26就无法精确地表示了。原因也很直观,0.26落在2^-1和2^-2之间,无法用二进制表示。

针对这种情况,IEEE有一系列的规定来约束使用什么样的二进制内容来表现这种无法精确用二进制表示的浮点数。这个规则不会在这篇文章中具体描述,可以阅读IEEE的相关资料获取信息。

什么是round-trip

如果从字面上理解就是”回路”的意思。这是IEEE规定的浮点数转换成字符串的一个约束条件,即如果将一个double转换成字符串,再将这个字符串转换成double, 转换后的double和原来的double二进制内容保持一致,则说明这个字符串转换满足round-trip. 上文已经说过,有些double数据是无法用二进制精确表示的,那么转换成可以阅读的字符串后再转换回来时,有可能就无法再还原原始的二进制内容了。

IEEE 指出,如果一个double要满足round-trip的条件,至少要有17位数字。具体的原因可以参考这篇文章。所以为了要让一个double数字满足round-trip, 在将double转换成字符串时至少要有17位。如果精度不足17位,需要想办法将其精度补足到17位。看以下的C#例子就可以比较直观地理解round-trip:

namespace roundtrip
{
   
    class Program
    {
   
        static void Main(string[] args)
        {
            double d = 1.11;

            string normal = d.ToString();
            string roundTrippable = d.ToString("G17");

            Console.WriteLine(normal);
            Console.WriteLine(roundTrippable);
        }
    }
}

输出如下:

1.11
1.1100000000000001

注意: 如果你查看老的msdn文档,会发现微软建议使用ToString(“R”)来保证round-trip. 但实际上使用”R”是有bug的,目前已经改为使用”G17”. 如果你想知道上下文,可以查看

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值