Почему конверсия туда и обратно с помощью строки, небезопасной для двойной?

Недавно мне пришлось сериализовать двойной текст, а затем вернуть его обратно. Значение кажется не эквивалентным:

double d1 = 0.84551240822557006; string s = d1.ToString("R"); double d2 = double.Parse(s); bool s1 = d1 == d2; // -> s1 is False 

Но согласно MSDN: Standard Numeric Format Strings , опция «R» должна гарантировать безопасность в оба конца.

Спецификатор формата round-trip («R») используется для проверки того, что числовое значение, которое преобразуется в строку, будет обработано обратно в одно и то же числовое значение

Почему это случилось?

Я нашел ошибку.

.NET делает следующее в clr\src\vm\comnumber.cpp :

 DoubleToNumber(value, DOUBLE_PRECISION, &number); if (number.scale == (int) SCALE_NAN) { gc.refRetVal = gc.numfmt->sNaN; goto lExit; } if (number.scale == SCALE_INF) { gc.refRetVal = (number.sign? gc.numfmt->sNegativeInfinity: gc.numfmt->sPositiveInfinity); goto lExit; } NumberToDouble(&number, &dTest); if (dTest == value) { gc.refRetVal = NumberToString(&number, 'G', DOUBLE_PRECISION, gc.numfmt); goto lExit; } DoubleToNumber(value, 17, &number); 

DoubleToNumber довольно прост – он просто вызывает _ecvt , который находится в среде выполнения C:

 void DoubleToNumber(double value, int precision, NUMBER* number) { WRAPPER_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 { char* src = _ecvt(value, precision, &number->scale, &number->sign); wchar* dst = number->digits; if (*src != '0') { while (*src) *dst++ = *src++; } *dst = 0; } } 

Оказывается, _ecvt возвращает строку 845512408225570 .

Обратите внимание на конечный ноль? Оказывается, все имеет значение!
Когда нуль присутствует, результат фактически анализирует обратно до 0.84551240822557006 , что является вашим первоначальным номером, поэтому он сравнивается с равным, и, следовательно, возвращается только 15 цифр.

Однако, если я 84551240822557 строку с нулевым значением до 84551240822557 , я возвращаюсь обратно 0.84551240822556994 , что не является вашим первоначальным номером, и, следовательно, оно вернет 17 цифр.

Доказательство. Запустите следующий 64-разрядный код (большая часть которого я извлек из CLI 2.0 общего уровня Microsoft) в вашем отладчике и проверим v в конце main :

 #include  #include  #include  #define min(a, b) (((a) < (b)) ? (a) : (b)) struct NUMBER { int precision; int scale; int sign; wchar_t digits[20 + 1]; NUMBER() : precision(0), scale(0), sign(0) {} }; #define I64(x) x##LL static const unsigned long long rgval64Power10[] = { // powers of 10 /*1*/ I64(0xa000000000000000), /*2*/ I64(0xc800000000000000), /*3*/ I64(0xfa00000000000000), /*4*/ I64(0x9c40000000000000), /*5*/ I64(0xc350000000000000), /*6*/ I64(0xf424000000000000), /*7*/ I64(0x9896800000000000), /*8*/ I64(0xbebc200000000000), /*9*/ I64(0xee6b280000000000), /*10*/ I64(0x9502f90000000000), /*11*/ I64(0xba43b74000000000), /*12*/ I64(0xe8d4a51000000000), /*13*/ I64(0x9184e72a00000000), /*14*/ I64(0xb5e620f480000000), /*15*/ I64(0xe35fa931a0000000), // powers of 0.1 /*1*/ I64(0xcccccccccccccccd), /*2*/ I64(0xa3d70a3d70a3d70b), /*3*/ I64(0x83126e978d4fdf3c), /*4*/ I64(0xd1b71758e219652e), /*5*/ I64(0xa7c5ac471b478425), /*6*/ I64(0x8637bd05af6c69b7), /*7*/ I64(0xd6bf94d5e57a42be), /*8*/ I64(0xabcc77118461ceff), /*9*/ I64(0x89705f4136b4a599), /*10*/ I64(0xdbe6fecebdedd5c2), /*11*/ I64(0xafebff0bcb24ab02), /*12*/ I64(0x8cbccc096f5088cf), /*13*/ I64(0xe12e13424bb40e18), /*14*/ I64(0xb424dc35095cd813), /*15*/ I64(0x901d7cf73ab0acdc), }; static const signed char rgexp64Power10[] = { // exponents for both powers of 10 and 0.1 /*1*/ 4, /*2*/ 7, /*3*/ 10, /*4*/ 14, /*5*/ 17, /*6*/ 20, /*7*/ 24, /*8*/ 27, /*9*/ 30, /*10*/ 34, /*11*/ 37, /*12*/ 40, /*13*/ 44, /*14*/ 47, /*15*/ 50, }; static const unsigned long long rgval64Power10By16[] = { // powers of 10^16 /*1*/ I64(0x8e1bc9bf04000000), /*2*/ I64(0x9dc5ada82b70b59e), /*3*/ I64(0xaf298d050e4395d6), /*4*/ I64(0xc2781f49ffcfa6d4), /*5*/ I64(0xd7e77a8f87daf7fa), /*6*/ I64(0xefb3ab16c59b14a0), /*7*/ I64(0x850fadc09923329c), /*8*/ I64(0x93ba47c980e98cde), /*9*/ I64(0xa402b9c5a8d3a6e6), /*10*/ I64(0xb616a12b7fe617a8), /*11*/ I64(0xca28a291859bbf90), /*12*/ I64(0xe070f78d39275566), /*13*/ I64(0xf92e0c3537826140), /*14*/ I64(0x8a5296ffe33cc92c), /*15*/ I64(0x9991a6f3d6bf1762), /*16*/ I64(0xaa7eebfb9df9de8a), /*17*/ I64(0xbd49d14aa79dbc7e), /*18*/ I64(0xd226fc195c6a2f88), /*19*/ I64(0xe950df20247c83f8), /*20*/ I64(0x81842f29f2cce373), /*21*/ I64(0x8fcac257558ee4e2), // powers of 0.1^16 /*1*/ I64(0xe69594bec44de160), /*2*/ I64(0xcfb11ead453994c3), /*3*/ I64(0xbb127c53b17ec165), /*4*/ I64(0xa87fea27a539e9b3), /*5*/ I64(0x97c560ba6b0919b5), /*6*/ I64(0x88b402f7fd7553ab), /*7*/ I64(0xf64335bcf065d3a0), /*8*/ I64(0xddd0467c64bce4c4), /*9*/ I64(0xc7caba6e7c5382ed), /*10*/ I64(0xb3f4e093db73a0b7), /*11*/ I64(0xa21727db38cb0053), /*12*/ I64(0x91ff83775423cc29), /*13*/ I64(0x8380dea93da4bc82), /*14*/ I64(0xece53cec4a314f00), /*15*/ I64(0xd5605fcdcf32e217), /*16*/ I64(0xc0314325637a1978), /*17*/ I64(0xad1c8eab5ee43ba2), /*18*/ I64(0x9becce62836ac5b0), /*19*/ I64(0x8c71dcd9ba0b495c), /*20*/ I64(0xfd00b89747823938), /*21*/ I64(0xe3e27a444d8d991a), }; static const signed short rgexp64Power10By16[] = { // exponents for both powers of 10^16 and 0.1^16 /*1*/ 54, /*2*/ 107, /*3*/ 160, /*4*/ 213, /*5*/ 266, /*6*/ 319, /*7*/ 373, /*8*/ 426, /*9*/ 479, /*10*/ 532, /*11*/ 585, /*12*/ 638, /*13*/ 691, /*14*/ 745, /*15*/ 798, /*16*/ 851, /*17*/ 904, /*18*/ 957, /*19*/ 1010, /*20*/ 1064, /*21*/ 1117, }; static unsigned DigitsToInt(wchar_t* p, int count) { wchar_t* end = p + count; unsigned res = *p - '0'; for ( p = p + 1; p < end; p++) { res = 10 * res + *p - '0'; } return res; } #define Mul32x32To64(a, b) ((unsigned long long)((unsigned long)(a)) * (unsigned long long)((unsigned long)(b))) static unsigned long long Mul64Lossy(unsigned long long a, unsigned long long b, int* pexp) { // it's ok to losse some precision here - Mul64 will be called // at most twice during the conversion, so the error won't propagate // to any of the 53 significant bits of the result unsigned long long val = Mul32x32To64(a >> 32, b >> 32) + (Mul32x32To64(a >> 32, b) >> 32) + (Mul32x32To64(a, b >> 32) >> 32); // normalize if ((val & I64(0x8000000000000000)) == 0) { val <<= 1; *pexp -= 1; } return val; } void NumberToDouble(NUMBER* number, double* value) { unsigned long long val; int exp; wchar_t* src = number->digits; int remaining; int total; int count; int scale; int absscale; int index; total = (int)wcslen(src); remaining = total; // skip the leading zeros while (*src == '0') { remaining--; src++; } if (remaining == 0) { *value = 0; goto done; } count = min(remaining, 9); remaining -= count; val = DigitsToInt(src, count); if (remaining > 0) { count = min(remaining, 9); remaining -= count; // get the denormalized power of 10 unsigned long mult = (unsigned long)(rgval64Power10[count-1] >> (64 - rgexp64Power10[count-1])); val = Mul32x32To64(val, mult) + DigitsToInt(src+9, count); } scale = number->scale - (total - remaining); absscale = abs(scale); if (absscale >= 22 * 16) { // overflow / underflow *(unsigned long long*)value = (scale > 0) ? I64(0x7FF0000000000000) : 0; goto done; } exp = 64; // normalize the mantisa if ((val & I64(0xFFFFFFFF00000000)) == 0) { val <<= 32; exp -= 32; } if ((val & I64(0xFFFF000000000000)) == 0) { val <<= 16; exp -= 16; } if ((val & I64(0xFF00000000000000)) == 0) { val <<= 8; exp -= 8; } if ((val & I64(0xF000000000000000)) == 0) { val <<= 4; exp -= 4; } if ((val & I64(0xC000000000000000)) == 0) { val <<= 2; exp -= 2; } if ((val & I64(0x8000000000000000)) == 0) { val <<= 1; exp -= 1; } index = absscale & 15; if (index) { int multexp = rgexp64Power10[index-1]; // the exponents are shared between the inverted and regular table exp += (scale < 0) ? (-multexp + 1) : multexp; unsigned long long multval = rgval64Power10[index + ((scale < 0) ? 15 : 0) - 1]; val = Mul64Lossy(val, multval, &exp); } index = absscale >> 4; if (index) { int multexp = rgexp64Power10By16[index-1]; // the exponents are shared between the inverted and regular table exp += (scale < 0) ? (-multexp + 1) : multexp; unsigned long long multval = rgval64Power10By16[index + ((scale < 0) ? 21 : 0) - 1]; val = Mul64Lossy(val, multval, &exp); } // round & scale down if ((unsigned long)val & (1 << 10)) { // IEEE round to even unsigned long long tmp = val + ((1 << 10) - 1) + (((unsigned long)val >> 11) & 1); if (tmp < val) { // overflow tmp = (tmp >> 1) | I64(0x8000000000000000); exp += 1; } val = tmp; } val >>= 11; exp += 0x3FE; if (exp <= 0) { if (exp <= -52) { // underflow val = 0; } else { // denormalized val >>= (-exp+1); } } else if (exp >= 0x7FF) { // overflow val = I64(0x7FF0000000000000); } else { val = ((unsigned long long)exp << 52) + (val & I64(0x000FFFFFFFFFFFFF)); } *(unsigned long long*)value = val; done: if (number->sign) *(unsigned long long*)value |= I64(0x8000000000000000); } int main() { NUMBER number; number.precision = 15; double v = 0.84551240822557006; char *src = _ecvt(v, number.precision, &number.scale, &number.sign); int truncate = 0; // change to 1 if you want to truncate if (truncate) { while (*src && src[strlen(src) - 1] == '0') { src[strlen(src) - 1] = 0; } } wchar_t* dst = number.digits; if (*src != '0') { while (*src) *dst++ = *src++; } *dst++ = 0; NumberToDouble(&number, &v); return 0; } 

Мне кажется, что это просто ошибка. Ваши ожидания вполне оправданы. Я воспроизвел его с помощью .NET 4.5.1 (x64), выполнив следующее консольное приложение, которое использует мой class DoubleConverter . DoubleConverter.ToExactString показывает точное значение, представленное double :

 using System; class Test { static void Main() { double d1 = 0.84551240822557006; string s = d1.ToString("r"); double d2 = double.Parse(s); Console.WriteLine(s); Console.WriteLine(DoubleConverter.ToExactString(d1)); Console.WriteLine(DoubleConverter.ToExactString(d2)); Console.WriteLine(d1 == d2); } } 

Результаты в .NET:

 0.84551240822557 0.845512408225570055719799711368978023529052734375 0.84551240822556994469749724885332398116588592529296875 False 

Результаты в Mono 3.3.0:

 0.84551240822557006 0.845512408225570055719799711368978023529052734375 0.845512408225570055719799711368978023529052734375 True 

Если вы вручную укажете строку из Mono (которая содержит «006» в конце), .NET проверит это обратно к исходному значению. Для него похоже, что проблема ToString("R") с обработкой ToString("R") а не с parsingом.

Как отмечено в других комментариях, похоже, что это специфично для работы в среде x64 CLR. Если вы компилируете и запускаете вышеуказанный целевой код x86, это нормально:

 csc /platform:x86 Test.cs DoubleConverter.cs 

… вы получаете те же результаты, что и с Mono. Было бы интересно узнать, появляется ли ошибка в RyuJIT – у меня нет того, что было установлено в данный момент. В частности, я могу представить, что это, возможно , ошибка JIT, или вполне возможно, что существуют целые различные реализации внутренних элементов double.ToString на основе архитектуры.

Я предлагаю вам указать ошибку на http://connect.microsoft.com.

В последнее время я пытаюсь решить эту проблему . Как указано в коде , double.ToString («R») имеет следующую логику:

  1. Попробуйте преобразовать double в строку с точностью 15.
  2. Преобразуйте строку обратно в двойное и сравните с исходным двойным. Если они одинаковы, мы возвращаем преобразованную строку с точностью 15.
  3. В противном случае преобразуйте double в строку с точностью 17.

В этом случае double.ToString («R») ошибочно выбрал результат с точностью 15, чтобы ошибка произошла. В документе MSDN есть официальное обходное решение:

В некоторых случаях двойные значения, форматированные с помощью стандартной строки числового формата «R», не выполняются в обратном направлении, если они скомпилированы с использованием переключателей / platform: x64 или / platform: anycpu и работают в 64-разрядных системах. Чтобы обойти эту проблему, вы можете форматировать двойные значения, используя строку стандартного числового формата «G17». В следующем примере используется строка формата «R» с двойным значением, которое не работает в обратном направлении, а также использует строку формата «G17» для успешного округления исходного значения.

Поэтому, если эта проблема не устранена, вам необходимо использовать double.ToString («G17») для кругового отключения.

Обновление : теперь существует определенная проблема для отслеживания этой ошибки.

Вау – трехлетний вопрос, и все, кажется, пропустили точку – даже Джон Скит! (@ Jon: Уважение. Надеюсь, я не одурачиваю себя).

Для записи я запустил образец кода и в моей среде (Win10 x64 AnyCPU Debug, target .NetFx 4.7) тест после обратной поездки вернул true.

Вот эксперимент. Цифры выровнены, чтобы помочь сделать точку …

Этот код …

  string Breakdown(double v) { var ret = new StringBuilder(); foreach (byte b in BitConverter.GetBytes(v)) ret.Append($"{b:X2} "); ret.Length--; return ret.ToString(); } { var start = "0.99999999999999"; var incr = 70; for (int i = 0; i < 10; i++) { var dblStr = start + incr.ToString(); var dblVal = double.Parse(dblStr); Console.WriteLine($"{dblStr} : {dblVal:N16} : {Breakdown(dblVal)} : {dblVal:R}"); incr++; } } Console.WriteLine(); { var start = 0.999999999999997; var incr = 0.0000000000000001; var dblVal = start; for (int i = 0; i < 10; i++) { Console.WriteLine($"{i,-18} : {dblVal:N16} : {Breakdown(dblVal)} : {dblVal:R}"); dblVal += incr; } } 

Производит этот вывод (затем добавляются звездочки ***) ...

  0.9999999999999970 : 0.9999999999999970 : E5 FF FF FF FF FF EF 3F : 0.999999999999997 0.9999999999999971 : 0.9999999999999970 : E6 FF FF FF FF FF EF 3F : 0.99999999999999711 0.9999999999999972 : 0.9999999999999970 : E7 FF FF FF FF FF EF 3F : 0.99999999999999722 0.9999999999999973 : 0.9999999999999970 : E8 FF FF FF FF FF EF 3F : 0.99999999999999734 *** 0.9999999999999974 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 *** 0.9999999999999975 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 0.9999999999999976 : 0.9999999999999980 : EA FF FF FF FF FF EF 3F : 0.99999999999999756 0.9999999999999977 : 0.9999999999999980 : EB FF FF FF FF FF EF 3F : 0.99999999999999767 0.9999999999999978 : 0.9999999999999980 : EC FF FF FF FF FF EF 3F : 0.99999999999999778 0.9999999999999979 : 0.9999999999999980 : ED FF FF FF FF FF EF 3F : 0.99999999999999789 0 : 0.9999999999999970 : E5 FF FF FF FF FF EF 3F : 0.999999999999997 1 : 0.9999999999999970 : E6 FF FF FF FF FF EF 3F : 0.99999999999999711 2 : 0.9999999999999970 : E7 FF FF FF FF FF EF 3F : 0.99999999999999722 3 : 0.9999999999999970 : E8 FF FF FF FF FF EF 3F : 0.99999999999999734 +++ 4 : 0.9999999999999970 : E9 FF FF FF FF FF EF 3F : 0.99999999999999745 5 : 0.9999999999999980 : EA FF FF FF FF FF EF 3F : 0.99999999999999756 6 : 0.9999999999999980 : EB FF FF FF FF FF EF 3F : 0.99999999999999767 7 : 0.9999999999999980 : EC FF FF FF FF FF EF 3F : 0.99999999999999778 8 : 0.9999999999999980 : ED FF FF FF FF FF EF 3F : 0.99999999999999789 9 : 0.9999999999999980 : EE FF FF FF FF FF EF 3F : 0.999999999999998 

Это делается искусственно, но в первом разделе цикл подсчитывается с шагом десятичного числа 0.0000000000000001.
Обратите внимание, что два «последовательных значения» (***) имеют одинаковое внутреннее двоичное представление.

Во втором разделе - потому что мы не прыгаем через обручи, чтобы заставить десятичное сложение - внутреннее значение продолжает тикать в наименее значимом бите. Две последовательности из 10 значений выходят из синхронизации после 5 итераций.

Дело в том, что (внутренне двоичные) двойники не могут иметь точных десятичных представлений и наоборот.
Мы можем только попытаться получить десятичную строку, представляющую наше значение «как можно ближе».
Здесь строка в формате R 0.99999999999999745 неоднозначно «ближе всего» к 0.9999999999999974 или 0.9999999999999975.

Я понимаю, что возникает вопрос: «Показывать эту функцию по-другому» (одно десятичное представление, отображающее неоднозначно, в два разных бинарных файла), но не сумело его воссоздать.
Мы все время правы на пределе точности двойников, и поэтому нужны строки в формате R.

Мне нравится думать об этом так: «Спецификатор формата round-trip формирует строку, представляющую ближайшее двойное значение, вашему двойному значению, которое может быть округлено». Другими словами, строка R-форматирования должна быть округленной, а не обязательно значение ».

Чтобы исправить эту точку, нельзя предполагать, что значение «value -> string -> same value» возможно, но
должен иметь возможность полагаться на «value -> string -> близкое значение -> ту же строку -> то же самое близкое значение -> ...

Запомнить

  1. Внутреннее представление удвоений зависит от среды / платформы

  2. Даже в полностью экосистеме Microsoft все еще есть много возможных вариантов

    а. Параметры сборки (x86 / x64 / AnyCPU, Release / Debug)

    б. Оборудование (процессоры Intel имеют 80-битный регистр для арифметики - которые могут использоваться по-разному с помощью кода отладки и выпуска)

    с. Кто знает, где может работать IL-код (32-разрядный режим до 64 бит в операционной системе X / Y и т. Д.)?

Это должно «исправить» код исходного вопроса ...

 double d1 = 0.84551240822557006; string s1 = d1.ToString("R"); double d2 = double.Parse(s1); // d2 is not necessarily == d1 string s2 = d2.ToString("R"); double d3 = double.Parse(s2); // you must get true here bool roundTripSuccess = d2 == d3; 
  • Какая стандартная (или самая лучшая) библиотека большого числа (произвольная точность) для Lua?
  • Давайте будем гением компьютера.