Почему преобразование туда и обратно через строку не безопасно для двойника?
недавно мне пришлось сериализовать двойник в текст, а затем вернуть его обратно. Значение, кажется, не эквивалентно:
double d1 = 0.84551240822557006;
string s = d1.ToString("R");
double d2 = double.Parse(s);
bool s1 = d1 == d2;
// -> s1 is False
а по MSDN: строки стандартного числового формата, вариант " R " должен гарантировать безопасность туда и обратно.
спецификатор формата туда и обратно ("R") используется для обеспечения того, что числовое значение, преобразованное в строку, будет проанализировано обратно в то же числовое значение
почему это случилось?
4 ответа:
я нашел ошибку.
.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
, тогда я вернусь0.84551240822556994
, которая составляет не ваш исходный номер, и, следовательно, он вернет 17 цифр.доказательство: запустите следующий 64-разрядный код (большую часть которого я извлек из Microsoft Shared Source CLI 2.0) в отладчике и проверьте
v
в концеmain
:#include <stdlib.h> #include <string.h> #include <math.h> #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), запустив следующее консольное приложение, которое использует my
DoubleConverter
класса.DoubleConverter.ToExactString
показывает точно значение, представленное adouble
: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
результаты в моно 3.3.0:
0.84551240822557006 0.845512408225570055719799711368978023529052734375 0.845512408225570055719799711368978023529052734375 True
если вы вручную укажете строку из Mono (которая содержит "006" на end), .NET будет анализировать это обратно к исходному значению. Похоже, что проблема заключается в
ToString("R")
работа, а не разбор.как отмечалось в других комментариях, похоже, что это относится к работе под x64 CLR. Если вы скомпилируете и запустите приведенный выше код, предназначенный для x86, все нормально:
csc /platform:x86 Test.cs DoubleConverter.cs
... вы получаете те же результаты, что и с моно. Было бы интересно узнать, появляется ли ошибка под RyuJIT - на данный момент у меня этого нет себя. В частности, я могу себе это представить возможно будучи ошибкой JIT, или вполне возможно, что существуют целые различные реализации внутренних элементов
double.ToString
на основе архитектуры.Я предлагаю вам подать ошибку в http://connect.microsoft.com
недавно Я пытаюсь решить эту проблему. Как указано код двойной.ToString ("R") имеет следующую логику:
- попробуйте преобразовать double в string с точностью до 15.
- преобразуйте строку обратно в double и сравните с исходным double. Если они одинаковы, мы возвращаем преобразованную строку, точность которой равна 15.
- в противном случае преобразуйте double в string с точностью 17.
в данном случае двойной.ToString ("R") неправильно выбрал результат с точностью 15, поэтому ошибка происходит. В документе MSDN есть официальный обходной путь:
в некоторых случаях двойные значения, отформатированные со строкой стандартного числового формата "R", не могут успешно выполняться в обоих направлениях при компиляции с использованием коммутаторов /platform:x64 или /platform:anycpu и запускаются на 64-разрядных системах. Чтобы обойти эту проблему, можно отформатировать двойные значения с помощью Строка стандартного числового формата "G17". В следующем примере используется строка формата " R "с двойным значением, которое не выполняется успешно в оба конца, а также используется строка формата" G17 " для успешного выполнения кругового перехода исходного значения.
поэтому, если эта проблема не будет решена, вы должны использовать double.ToString ("G17") для кругл-задействовать.
обновление: теперь есть конкретный вопрос для отслеживания этой ошибки.
Вау - 3-летний вопрос, и все, кажется, пропустили точку-даже Джон Скит! (@Джон: Уважение. Надеюсь, я не выставляю себя дураком.)
для записи я запустил образец кода и в моем окружении (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
это делается искусственно, но в 1-м разделе цикл подсчитывается с шагом десятичной 0.0000000000000001.
Обратите внимание, что два "последовательных значения" (***) имеют одинаковое внутреннее двоичное представление.во 2-м разделе-потому что мы не прыгаем через обручи, чтобы заставить десятичное сложение-внутреннее значение продолжает тикать в наименее значительном бите. Две последовательности из 10 значений выходят из синхронизации после 5 итераций.
дело в том, что (внутренне двоичные) двойники не могут иметь точных десятичных представлений и наоборот.
Мы можем только попытаться получить десятичную строку, представляющую наше значение "как можно ближе".
Здесь R-форматированная строка 0.99999999999999745 неоднозначно "ближе всего" либо 0.9999999999999974 или 0.9999999999999975.Я ценю, что вопрос, похоже, "показывает эту функцию наоборот" (один десятичный отображение представления неоднозначно на два разных двоичных файла), но не удалось воссоздать это.
В конце концов, мы находимся на пределе точности двойников, и именно поэтому необходимы строки R-формата.мне нравится думать об этом таким образом " спецификатор формата туда и обратно возвращает строку представляет ближайшее двойное значение к вашему двойному значению, которое может быть отключено. "другими словами" R-форматированная строка должна быть туда-обратно-в состоянии, не обязательно значение."
чтобы работать с точкой, не следует предполагать, что" value -> string -> same value " возможно, но
должен быть в состоянии полагаться на " значение - > строка - > соседнее значение - > та же строка - > то же самое соседнее значение ->...помните
внутреннее представление двойников зависит от среды / платформы
даже в полностью-экосистема Microsoft есть еще много возможных вариантов
a. параметры сборки (x86 / x64 / AnyCPU, Release / Debug)
B. аппаратное обеспечение (процессоры Intel имеют 80-битный регистр для арифметики - который может использоваться по-разному при отладке и выпуске кода сборки)
c. кто знает, где код 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;