重新實現.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”. 如果你想知道上下文,可以查看這個PR這個issue.

重寫double.ToString()的核心算法

有了前面的理論基礎,我們可以着手開始重寫double.ToString()了。但是,並不是簡單地實現這篇論文就能夠滿足需求,還需要將這個算法完美地嵌入到 .NET Core的代碼中。

dragon4算法簡介

Robert的這篇論文的算法有個別名,叫做dragon4. 別名的來歷也很有趣,有興趣的朋友可以去查一下。

算法的核心思想如下:

  1. 爲了保持精度,需要將double用整形來表示。可以想到,double可以用分子和分母兩個整數表示。這樣一來,在轉換過程中,所有的計算都是整數算數運算,不會損失精度。
  2. 使用分子和分母兩個整數來表示double帶來了另一個問題。由於double由64位二進制數字表示,那麼分子或分母都有可能會超過64位整數(爲什麼:)?)。即便用unsigned long long也無法表示分子和分母。所以只能擯棄內置的數據類型,自己實現BigNum數據類型,用於表示分子和分母。
  3. 轉換後的字符串實際上存有2個信息,一個是具體數字是什麼,一個是小數點的位置。
  4. 數字可以用BigNum的除法計算得來,但是應該保留多少位數字呢?論文中有2種實現,一種是free format, 意思是”一直輸出數字,直到能夠唯一表示這個double爲止” (這是一個比較複雜的判斷,這篇文章不詳述)。另一種是fixed format, 意思是你要告訴算法,你期望輸出多少位數字。.NET Core目前使用的是fixed format方式,所以我們只需要實現fixed format方式就可以了。
  5. 小數點位置的判斷

準備工作: 實現BigNum

C++並沒有內置的BigNum實現,爲了實現BigNum, 我們可以使用一個unsigned int的數組來表示數字。例如

static const UINT32 BIGSIZE = 35;
UINT32 m_blocks[BIGSIZE];

BigNum可以表示爲:

m_blocks[0] << 32 * 0 + m_blocks[1] << 32 * 1 + ...

具體的BigNum實現可以看bignum.hbignum.cpp

dragon4算法實現概述

首先我們需要排除特殊數字。比如NaN, 0, Infinite (具體的概念不在這裏闡述,可以查看IEEE對double的定義).

對於NaN和Infinite, .NET Core原本的代碼已經進行過特殊處理,即把存放數字的數組清空:

void DoubleToNumber(double value, int precision, NUMBER* number)
{
    WRAPPER_NO_CONTRACT
    _ASSERTE(number != NULL);

    number->precision = precision;
    if (((FPDOUBLE*)&value)->exp == 0x7FF) {
        number->scale = (((FPDOUBLE*)&value)->mantLo || ((FPDOUBLE*)&value)->mantHi) ? SCALE_NAN: SCALE_INF;
        number->sign = ((FPDOUBLE*)&value)->sign;
        number->digits[0] = 0;
    }
    else {
        DoubleToNumberWorker(value, precision, &number->scale, &number->sign, number->digits);
    }
}

對於0, 可以在進入核心算法之前過濾掉:

// Shortcut for zero.
if (value == 0.0)
{
    *dec = 0;
    *sign = 0;

    // Instead of zeroing digits, we just make it as an empty string due to performance reason.
    *digits = 0;

    return;
} 

這裏爲了性能用了一個技巧,原本根據論文,digits應該按照要求的精度填充等長的0,但實際情況下這麼做浪費了填充內存的時間,直接把digits賦值爲空數組既不影響後續使用,也節約了時間。雖然NaN和Infinite也把digits設置爲了空數組, 但是通過exponent和mantissa可以輕易區分0, NaN和Infinite.

接下來進入算法的主步驟:

首先我們需要按照論文把fraction和biased exponent計算出來:

 // Step 1:
// Extract meta data from the input double value.
//
// Refer to IEEE double precision floating point format.
UINT64 f = 0;
int e = 0;
UINT32 mantissaHighBitIdx = 0;
if (((FPDOUBLE*)&value)->exp != 0)
{
    // For normalized value, according to https://en.wikipedia.org/wiki/Double-precision_floating-point_format
    // value = 1.fraction * 2^(exp - 1023) 
    //       = (1 + mantissa / 2^52) * 2^(exp - 1023) 
    //       = (2^52 + mantissa) * 2^(exp - 1023 - 52)
    //
    // So f = (2^52 + mantissa), e = exp - 1075; 
    f = ((UINT64)(((FPDOUBLE*)&value)->mantHi) << 32) | ((FPDOUBLE*)&value)->mantLo + ((UINT64)1 << 52);
    e = ((FPDOUBLE*)&value)->exp - 1075;
    mantissaHighBitIdx = 52;
}
else
{
    // For denormalized value, according to https://en.wikipedia.org/wiki/Double-precision_floating-point_format
    // value = 0.fraction * 2^(1 - 1023)
    //       = (mantissa / 2^52) * 2^(-1022)
    //       = mantissa * 2^(-1022 - 52)
    //       = mantissa * 2^(-1074)
    // So f = mantissa, e = -1074
    f = ((UINT64)(((FPDOUBLE*)&value)->mantHi) << 32) | ((FPDOUBLE*)&value)->mantLo;
    e = -1074;
    mantissaHighBitIdx = BigNum::LogBase2(f);
}

接下來估算小數位數

 // Step 2:
// Estimate k. We'll verify it and fix any error later.
//
// This is an improvement of the estimation in the original paper.
// Inspired by http://www.ryanjuckett.com/programming/printing-floating-point-numbers/
//
// LOG10V2 = 0.30102999566398119521373889472449
// DRIFT_FACTOR = 0.69 = 1 - log10V2 - epsilon (a small number account for drift of floating point multiplication)
int k = (int)(ceil(double((int)mantissaHighBitIdx + e) * LOG10V2 - DRIFT_FACTOR));

// Step 3:
// Store the input double value in BigNum format.
//
// To keep the precision, we represent the double value as r/s.
// We have several optimization based on following table in the paper.
//
//     ----------------------------------------------------------------------------------------------------------
//     |               e >= 0                   |                         e < 0                                 |
//     ----------------------------------------------------------------------------------------------------------
//     |  f != b^(P - 1)  |  f = b^(P - 1)      | e = min exp or f != b^(P - 1) | e > min exp and f = b^(P - 1) |
// --------------------------------------------------------------------------------------------------------------
// | r |  f * b^e * 2     |  f * b^(e + 1) * 2  |          f * 2                |            f * b * 2          |
// --------------------------------------------------------------------------------------------------------------
// | s |        2         |        b * 2        |          b^(-e) * 2           |            b^(-e + 1) * 2     |
// --------------------------------------------------------------------------------------------------------------  
//
// Note, we do not need m+ and m- because we only support fixed format input here.
// m+ and m- are used for free format input, which need to determine the exact range of values 
// that would round to value when input so that we can generate the shortest correct digits.
//
// In our case, we just output digits until reaching the expected precision. 
BigNum r(f);
BigNum s;
if (e >= 0)
{
    // When f != b^(P - 1):
    // r = f * b^e * 2
    // s = 2
    // value = r / s = f * b^e * 2 / 2 = f * b^e / 1
    //
    // When f = b^(P - 1):
    // r = f * b^(e + 1) * 2
    // s = b * 2
    // value = r / s =  f * b^(e + 1) * 2 / b * 2 = f * b^e / 1
    //
    // Therefore, we can simply say that when e >= 0:
    // r = f * b^e = f * 2^e
    // s = 1

    r.ShiftLeft(e);
    s.SetUInt64(1);
}
else
{
    // When e = min exp or f != b^(P - 1):
    // r = f * 2
    // s = b^(-e) * 2
    // value = r / s = f * 2 / b^(-e) * 2 = f / b^(-e)
    //
    // When e > min exp and f = b^(P - 1):
    // r = f * b * 2
    // s = b^(-e + 1) * 2
    // value = r / s =  f * b * 2 / b^(-e + 1) * 2 = f / b^(-e)
    //
    // Therefore, we can simply say that when e < 0:
    // r = f
    // s = b^(-e) = 2^(-e)

    BigNum::ShiftLeft(1, -e, s);
}

// According to the paper, we should use k >= 0 instead of k > 0 here.
// However, if k = 0, both r and s won't be changed, we don't need to do any operation.
//
// Following are the Scheme code from the paper:
// --------------------------------------------------------------------------------
// (if (>= est 0)
// (fixup r (∗ s (exptt B est)) m+ m− est B low-ok? high-ok? )
// (let ([scale (exptt B (− est))])
// (fixup (∗ r scale) s (∗ m+ scale) (∗ m− scale) est B low-ok? high-ok? ))))
// --------------------------------------------------------------------------------
//
// If est is 0, (∗ s (exptt B est)) = s, (∗ r scale) = (* r (exptt B (− est)))) = r.
//
// So we just skip when k = 0.

if (k > 0)
{
    BigNum poweredValue;
    BigNum::Pow10(k, poweredValue);
    s.Multiply(poweredValue);
}
else if (k < 0)
{
    BigNum poweredValue;
    BigNum::Pow10(-k, poweredValue);
    r.Multiply(poweredValue);
}

if (BigNum::Compare(r, s) >= 0)
{
    // The estimation was incorrect. Fix the error by increasing 1.
    k += 1;
}
else
{
    r.Multiply10();
}

*dec = k - 1;

接下來算出除最後一位的所有數字,如果數字不足以填滿需要的位數,會在後面的代碼中補0.

// Step 4:
// Calculate digits.
//
// Output digits until reaching the last but one precision or the numerator becomes zero.
int digitsNum = 0;
int currentDigit = 0;
while (true)
{
    currentDigit = BigNum::HeuristicDivide(&r, s);
    if (r.IsZero() || digitsNum + 1 == count)
    {
        break;
    }

    digits[digitsNum] = L'0' + currentDigit;
    ++digitsNum;

    r.Multiply10();
}

最後一位數字涉及到應該向上近似還是向下近似的問題,策略是”向最接近的數字近似”。

// Step 5:
// Set the last digit.
//
// We round to the closest digit by comparing value with 0.5:
//  compare( value, 0.5 )
//  = compare( r / s, 0.5 )
//  = compare( r, 0.5 * s)
//  = compare(2 * r, s)
//  = compare(r << 1, s)
r.ShiftLeft(1);
int compareResult = BigNum::Compare(r, s);
bool isRoundDown = compareResult < 0;

// We are in the middle, round towards the even digit (i.e. IEEE rouding rules)
if (compareResult == 0)
{
    isRoundDown = (currentDigit & 1) == 0;
}

if (isRoundDown)
{
    digits[digitsNum] = L'0' + currentDigit;
    ++digitsNum;
}
else
{
    wchar_t* pCurDigit = digits + digitsNum;

    // Rounding up for 9 is special.
    if (currentDigit == 9)
    {
        // find the first non-nine prior digit
        while (true)
        {
            // If we are at the first digit
            if (pCurDigit == digits)
            {
                // Output 1 at the next highest exponent
                *pCurDigit = L'1';
                ++digitsNum;
                *dec += 1;
                break;
            }

            --pCurDigit;
            --digitsNum;
            if (*pCurDigit != L'9')
            {
                // increment the digit
                *pCurDigit += 1;
                ++digitsNum;
                break;
            }
        }
    }
    else
    {
        // It's simple if the digit is not 9.
        *pCurDigit = L'0' + currentDigit + 1;
        ++digitsNum;
    }
}

如果數字的個數不足以填滿要求的精度,則用0來補充。

 while (digitsNum < count)
{
    digits[digitsNum] = L'0';
    ++digitsNum;
}

digits[count] = 0;

最後保存符號位:

*sign = ((FPDOUBLE*)&value)->sign;

優化算法

在上述算法實現過程中,有些地方沒有完全遵照論文的設計,而是做了一些優化。特別感謝這篇文章, 裏面涉及的啓發式除法提升了輸出數字的效率,而powerTable大大提升了power10的計算效率。

利用啓發式除法提升效率

在輸出數字的過程中,我們需要使用BigNum的除法,將數字一個一個地取出來。BigNum的除法是相當昂貴的操作符,所以整個算法最耗時的地方就在這裏。如果按照原論文的做法,雖然整體性能有提升,但是好不容易帶來的性能提升會在這裏被浪費掉,非常不划算。

有沒有什麼辦法能夠優化取數字的過程呢?這篇文章提出了啓發式除法算法, 我也將這個算法包含在了double.ToString()的實現中,大大提升了性能。

啓發式除法會有一篇專門的文章來講解,這裏只粗略地介紹它的概念。

因爲我們使用分子(假設爲numerator)和分母(假設爲denominator)來表示double, 即:

doubleValue = numerator / denominator

而numerator和denominator都是BigNum類, 執行除法需要遍歷每個BigNum內部的m_blocks, 這就非常耗時了。如果能夠把這種高精度的除法轉化爲普通的32位整數的除法,就可以大大提升效率。啓發式除法就是利用這種思想,只取numerator和denominator的最高位block的數字(unsigned int)做除法,只要滿足一定條件,這樣得出的商和完整執行BigNum的除法是一樣的。

爲什麼這樣是可行的?我們以a來表示numerator最高位block的unsigned int, b表示denominator最高位block的unsigned int. 從直觀感受上來說,執行除法時,高位的數字對商的影響最大。

首先可以輕鬆得出一個直觀的不等式:

floor(floor(a)/(floor(b) + 1)) < quotient

因爲分母增加了1,所以得出來的商肯定比原來的商小。另外,在前面的計算中, 我們會保證numerator < 10 * denominator (只有這樣,才能方便地計算小數位數,例如9.999… 如果numerator >= 10 * denominator了,就會出現10.0001之類的數字,而我們期待輸出1.00001).

如果floor(floor(a)/(floor(b) + 1)) - quotient < 1, 我們就可以正大光明地用最高位block的unsigned int相除取floor來得到真正的商了。

爲了方便計算,我們需要想辦法將quotient用a和b來表示。既然a表示numerator最高位block的值,那麼a = numerator >> 32 * (n - 1) = numerator / (2^32 (n-1)), 其中n表示有多少個block組成numerator, 每個block是一個unsigned int即32位。同理b = denominator >> 32 (n - 1) = denominator / (2^32 * (n-1)). 所以:

a / b = numerator / denominator

所以只要floor(floor(a)/(floor(b) + 1)) - a / b < 1, 就滿足我們的條件了。

什麼樣的a和b纔會保證上述不等式成立呢?結論是當 8 <= b < 9 時。如果b不在這個範圍內,則需要擴大numerator和denominator, 使得b落在這個範圍內。8 <= b < 9的推導將會有另外的一篇文章詳細講解。

在具體實現的時候,PrepareHeuristicDivide保證8 <= b <9, HeuristicDivide實現具體的啓發式除法算法。

利用powerTable提升效率

在整個計算過程中,經常會使用到10的n次方這樣的數字。如果每次都重複計算將影響效率,特別是當n足夠大,結果爲BigInteger的時候。我們可以採用空間換時間的方式,預先計算好10的n次方結果,並將其保存在數組中:

static constexpr UINT32 m_power10UInt32Table[UINT32POWER10NUM] = 
{
        1,          // 10^0
        10,         // 10^1
        100,        // 10^2
        1000,       // 10^3
        10000,      // 10^4
        100000,     // 10^5
        1000000,    // 10^6
        10000000,   // 10^7
};

static BigNum m_power10BigNumTable[BIGPOWER10NUM];

// 10^8
m_power10BigNumTable[0].m_len = (UINT32)1;
m_power10BigNumTable[0].m_blocks[0] = (UINT32)100000000;

// 10^16
m_power10BigNumTable[1].m_len = (UINT32)2;
m_power10BigNumTable[1].m_blocks[0] = (UINT32)0x6fc10000;
m_power10BigNumTable[1].m_blocks[1] = (UINT32)0x002386f2;

// 10^32
m_power10BigNumTable[2].m_len = (UINT32)4;
m_power10BigNumTable[2].m_blocks[0] = (UINT32)0x00000000;
m_power10BigNumTable[2].m_blocks[1] = (UINT32)0x85acef81;
m_power10BigNumTable[2].m_blocks[2] = (UINT32)0x2d6d415b;
m_power10BigNumTable[2].m_blocks[3] = (UINT32)0x000004ee;

// 10^64
m_power10BigNumTable[3].m_len = (UINT32)7;
m_power10BigNumTable[3].m_blocks[0] = (UINT32)0x00000000;
m_power10BigNumTable[3].m_blocks[1] = (UINT32)0x00000000;
m_power10BigNumTable[3].m_blocks[2] = (UINT32)0xbf6a1f01;
m_power10BigNumTable[3].m_blocks[3] = (UINT32)0x6e38ed64;
m_power10BigNumTable[3].m_blocks[4] = (UINT32)0xdaa797ed;
m_power10BigNumTable[3].m_blocks[5] = (UINT32)0xe93ff9f4;
m_power10BigNumTable[3].m_blocks[6] = (UINT32)0x00184f03;

// 10^128
m_power10BigNumTable[4].m_len = (UINT32)14;
m_power10BigNumTable[4].m_blocks[0] = (UINT32)0x00000000;
m_power10BigNumTable[4].m_blocks[1] = (UINT32)0x00000000;
m_power10BigNumTable[4].m_blocks[2] = (UINT32)0x00000000;
m_power10BigNumTable[4].m_blocks[3] = (UINT32)0x00000000;
m_power10BigNumTable[4].m_blocks[4] = (UINT32)0x2e953e01;
m_power10BigNumTable[4].m_blocks[5] = (UINT32)0x03df9909;
m_power10BigNumTable[4].m_blocks[6] = (UINT32)0x0f1538fd;
m_power10BigNumTable[4].m_blocks[7] = (UINT32)0x2374e42f;
m_power10BigNumTable[4].m_blocks[8] = (UINT32)0xd3cff5ec;
m_power10BigNumTable[4].m_blocks[9] = (UINT32)0xc404dc08;
m_power10BigNumTable[4].m_blocks[10] = (UINT32)0xbccdb0da;
m_power10BigNumTable[4].m_blocks[11] = (UINT32)0xa6337f19;
m_power10BigNumTable[4].m_blocks[12] = (UINT32)0xe91f2603;
m_power10BigNumTable[4].m_blocks[13] = (UINT32)0x0000024e;

// 10^256
m_power10BigNumTable[5].m_len = (UINT32)27;
m_power10BigNumTable[5].m_blocks[0] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[1] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[2] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[3] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[4] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[5] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[6] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[7] = (UINT32)0x00000000;
m_power10BigNumTable[5].m_blocks[8] = (UINT32)0x982e7c01;
m_power10BigNumTable[5].m_blocks[9] = (UINT32)0xbed3875b;
m_power10BigNumTable[5].m_blocks[10] = (UINT32)0xd8d99f72;
m_power10BigNumTable[5].m_blocks[11] = (UINT32)0x12152f87;
m_power10BigNumTable[5].m_blocks[12] = (UINT32)0x6bde50c6;
m_power10BigNumTable[5].m_blocks[13] = (UINT32)0xcf4a6e70;
m_power10BigNumTable[5].m_blocks[14] = (UINT32)0xd595d80f;
m_power10BigNumTable[5].m_blocks[15] = (UINT32)0x26b2716e;
m_power10BigNumTable[5].m_blocks[16] = (UINT32)0xadc666b0;
m_power10BigNumTable[5].m_blocks[17] = (UINT32)0x1d153624;
m_power10BigNumTable[5].m_blocks[18] = (UINT32)0x3c42d35a;
m_power10BigNumTable[5].m_blocks[19] = (UINT32)0x63ff540e;
m_power10BigNumTable[5].m_blocks[20] = (UINT32)0xcc5573c0;
m_power10BigNumTable[5].m_blocks[21] = (UINT32)0x65f9ef17;
m_power10BigNumTable[5].m_blocks[22] = (UINT32)0x55bc28f2;
m_power10BigNumTable[5].m_blocks[23] = (UINT32)0x80dcc7f7;
m_power10BigNumTable[5].m_blocks[24] = (UINT32)0xf46eeddc;
m_power10BigNumTable[5].m_blocks[25] = (UINT32)0x5fdcefce;
m_power10BigNumTable[5].m_blocks[26] = (UINT32)0x000553f7;

後記

新的實現使得double.ToString()在Linux和Windows上都快了3倍。新的double.ToString()將在.NET Core 2.1.0發佈時正式和大家見面。最後附上性能對比數據:

micro-benchmark程序:

using System;
using System.Diagnostics;
using System.Globalization;

namespace hlold
{
    class Program
    {
        static void Main(string[] args)
        {
            double number = 104234.343;
            CultureInfo cultureInfo = new CultureInfo("fr");
            for (int i = 0; i < 20; i++)
            {
                Stopwatch watch = new Stopwatch();
                watch.Start();
                for (int j = 0; j < 10000; j++)
                {
                    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);
                }

                Console.Write(watch.ElapsedMilliseconds);
                Console.WriteLine();
            }
        }
    }
}

新舊實現效率對比:

Output of new implementation:

52
52
50
51
50
51
51
51
51
50
50
50
51
50
50
59
50
50
51
51

Output of old implementation:

176
195
182
193
186
190
186
189
184
189
179
193
187
193
181
191
193
185
198
180
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章