Почему разные языки по-разному отображают число 9223372036854775807, хотя все используют один и тот же формат 8-байтного double для представления чисел?
9223372036854775807 - в коде
9223372036854775808 - C++ http://ideone.com/PV5iPg и http://codepad.org/vhQzDMqT
9223372036854776000 - Javascript https://jsfiddle.net/5ugL4rqh/
9223372036854776000 - Java http://ideone.com/QtXRWi
9223372036854780000 - C# http://ideone.com/36Lzzi
Ответы
Ответ 1
Здесь в каждой среде/языке два преобразования:
Из константы в исходном коде в объект в памяти
Печать этого объекта памяти выбранным способом.
C++
Эффект от кода из вопроса для
С++: volatile double x = 9223372036854775807.; схож
с
gcc's -ffloat-store опцией и
позволяет забыть
о
возможных дополнительных битах и
думать только о 64-битных IEEE
754
числах двойной точности,
используемые в рассматриваемой реализации (IEEE 754 не
обязателен, но конкретная реализация для float чисел должна быть
задокументирована).
Константа 9223372036854775807. из исходного кода превращается в
9223372036854775808. double
(ожидаемо для этого типа, см. демонстрацию битового представления внизу). В CPython тоже самое
происходит:
>>> 9223372036854775807. .as_integer_ratio()
(9223372036854775808, 1)
>>> 9223372036854775807. .hex()
'0x1.0000000000000p+63'
то есть 9223372036854775807. не может быть точно представлено в IEEE
754 double и поэтому используется приближение 9223372036854775808.
(263), которое уже выводится точно в этом случае с
помощью: cout << fixed << x; как ascii-строка:
"9223372036854775808.000000" (в C локали).
Как double в памяти и в виде бит в IEEE 754 представлен, и как печать может происходить в С, подробно описано в ответе на вопрос printf как средство печати переменных в С.
В данном случае, так как число является степенью двойки, то легко найти его IEE
754 представление:
d = ±знак · (1 + мантисса / 252) · 2порядок − 1023
знаковый бит равен нулю, так как число положительное
порядок = (63 + 1023)10 = 100001111102, чтобы получить 263
у мантисса все явные 52 бита нулевые (старший неявный 53ий бит всегда равен единице)
Все биты числа вместе:
0 10000111110 0000000000000000000000000000000000000000000000000000
Что подтверждается вычислениями на Питоне:
>>> import struct
>>> struct.pack('>d', 9223372036854775808.0).hex()
43e0000000000000
>>> bin(struct.unpack('>Q', struct.pack('>d', 9223372036854775808.0))[0])[2:].zfill(64)
'0100001111100000000000000000000000000000000000000000000000000000'
И в обратную сторону:
>>> b64 = 0b0_10000111110_0000000000000000000000000000000000000000000000000000 .to_bytes(8, 'big')
>>> b64.hex()
'43e0000000000000'
>>> "%f" % struct.unpack('>d', b64)[0]
'9223372036854775808.000000'
Порядок байт в памяти у числа в примере показан от старшего к
младшему (big-endian), но фактически может быть и от младшего к
старшему (little-endian):
>>> struct.pack('d', 9223372036854775808.0).hex()
'000000000000e043'
Можно посмотреть, что не подряд идут представимые числа,
вычитая/прибавляя по одному биту к мантиссе:
>>> x = 0b0_10000111110_0000000000000000000000000000000000000000000000000000
>>> def to_float_string(bits):
... return "%f" % struct.unpack('>d', bits.to_bytes(8, 'big'))[0]
>>> for n in range(x-1, x+2):
... print(to_float_string(n))
9223372036854774784.000000
9223372036854775808.000000
9223372036854777856.000000
Разница в один бит для чисел этой величины ведёт к разнице больше
тысячи в десятичном представлении: ..4784, ..5808, ..7856.
Можно воспользоваться C99 функцией nextafter():
#include
#include
#include
int main(void)
{
volatile double x = 9223372036854775808.0;
printf("%f\n", nextafter(x, DBL_MIN));
printf("%f\n", x);
printf("%f\n", nextafter(x, DBL_MAX));
}
Результаты совпадают с предыдущими:
9223372036854774784.000000
9223372036854775808.000000
9223372036854777856.000000
Javascript
Числа в JavaScript представлены интересным способом — целые как IEEE 754 double представлены. К примеру, максимальное число (Number.MAX_SAFE_INTEGER) равно 253.
9223372036854775807 на три порядка больше MAX_SAFE_INTEGER поэтому нет гарантии
что n и n+1 представимы.
> 9223372036854776000 === 9223372036854775807
true
> 9223372036854775808 === 9223372036854775807
true
9223372036854776000
(результат document.write(9223372036854775807) в одной из javascript реализаций)
допустим
cтандартом в качестве строкового представления для 9223372036854775807 (это по-прежнему одно binary64 число: 0x1.0000000000000p+63).
Результаты побитовых операций вообще ограничены 32-битными числами со знаком. Можн
посмотреть на какие ухищрения пришлость пойти, чтобы воспроизвести результат хэш-функции, реализованной в javascript: Как перевести из Javascript в Питон функцию хэширования строки.
Java
В Java, double это тип со значениями, которые включают 64-bit IEEE 754 числа с плавающей точкой.
double x = 9223372036854775807.;
System.out.format("%f", x);
Возможная логика, почему 9223372036854776000, а не
9223372036854775808. десятичное представление выбрано для binary64
числа 0x1.0000000000000p+63 в том, что в общем случае это позволяет
меньше цифр печатать для дробных чисел — не отображаются завершающие
нули (это спекуляция — я не углублялся в этот вопрос).
C#
msdn утверждает
что double в C# соотвествует IEEE 754.
9223372036854780000.0 намекает, что Console.WriteLine("{0:0.0}", x); округляет до 15 цифр при печати. Напечатанное число отличается от x:
>>> 9223372036854780000. .hex()
'0x1.0000000000002p+63'
Вероятно это происходит по cхожей причине, что и 0.1 показывается как 0.1 при печати
а не 0.10000000000000001 или вообще 0.100000000000000005551115123125782702118158340454101562
( 0.1 .hex() == '0x1.999999999999ap-4'). Более того binary64 представление другое (263 vs. 263+212) (единственный из представленных примеров в вопросе, который по умолчанию не выводит эквивалентное представление).
Возможно, приоритет в округлении до 15 цифр, не обращая внимание достаточно ли это
чтобы эквивалентное binary64 представление получить. Не только C# себя так ведёт, к примеру, numpy.array в Питоне выводит по умолчанию 8 цифр:
>>> import numpy
>>> a = numpy.array([2**63-1, 2**63, 2**63+2**12], dtype=numpy.float64)
>>> a
array([ 9.22337204e+18, 9.22337204e+18, 9.22337204e+18])
>>> 9.22337204e+18 .hex()
'0x1.0000000176f0ap+63'
>>> numpy.set_printoptions(precision=16)
>>> a
array([ 9.2233720368547758e+18, 9.2233720368547758e+18,
9.2233720368547799e+18])
Показ 17 цифр в C# возможен с помощью
стандартного "{0:R}" формата, что выводит
9.2233720368547758E+18, то есть снова ту же рассматриваемую изначальную
263 степень получили:
>>> 9.2233720368547758E+18 .hex()
'0x1.0000000000000p+63'
Ответ 2
Последовательные числа, представимые в формате IEEE 754 double, в окрестности 9223372036854775807 это
9223372036854774784
9223372036854775808
9223372036854777856
Ближайшим является 9223372036854775808.
А разница в выводе возникает только на этапе формирования десятичного представлени
двоичного плавающего IEEE 754 числа. Внутренне все вышеупомянутые реализации языко
используют IEEE 754 double, значение которого во всех случаях равно именно 9223372036854775808, в чем легко убедиться путем вычитания из хранимого числа его старших разрядов.
Например в вашем C# примере достаточно вместо x напечатать x - 9223000000000000000, как "недостающие" младшие разряды сразу "проявятся": http://ideone.com/H5eKPe
Абсолютно то же самое происходит и в Javascript.
Ответ 3
Это задача с разряда, почему 0.1 + 0.1 не всегда точно 0.2. Если посмотреть в википедию
то станет понятно, что числа двойной точности имеют мантису в 52 бита. 52 бита дают 15-16 цифр. А у Вас их больше.
UPD
Нашел чудный сервис для просмотра чисел двойной точности http://www.binaryconvert.com/convert_double.html
Вбиваем в него 9223372036854775807 и 9223372036854775808, и даже 922337203685477580
и видим, что у них всех одно и тоже бинарное представление - 0x43E0000000000000. Обратное преобразование для него дает 9.223372036854775808E18 . То есть, данный формат не может различить три заданных числа. Это отвечает на первую часть вопроса.
Почему же разные языки по разному отображают это число? Во первых, каждый из языко
использует какую то свою дефолтно настроенную систему для форматирования вывода. А чт
именно ели/пили разработчики языка/компилятора/платформы в этот момент, мы не знаем. Во вторых, каждый разработчик пытается сделать свой, единственно верный и правильный парсер-преобразователь.
Посмотрим на вывод java и пропустим его через конвертор 9223372036854776000 => 0x43E000000000000
- видим то же самое представление. Видимо алгоритм java другой. Его видимо использует и javascript (с ним правда не все очевидно - есть много различных реализаций, но все проверенные мной варианты на видне/линуксе давали один и тот же результат).
А вот у шарпа тут похоже бага: 9223372036854780000 => 0x43E0000000000002 (двойк
в конце). Но потом они видимо исправились и ввели специальный вид форматирования - round trip format
Console.WriteLine("{0:R}", x);
и в этом случае имеем 9.2233720368547758E+18, что эквивалентно 9223372036854775800
В бинарном представлении это будет все тот же 0x43E0000000000000. Заставить вывести сразу в правильной форме не получилось :(
Вывод. Все, кроме шарпа вывели правильно. Абсолютно правильно. Просто, по исторически
(и видимо патентным/велосипедным правилам) используют разные алгоритмы предобразования. Шарп выделился, но думаю, что на то есть исторические причины (вплоть до оптимизации по скорости или просто человеческая ошибка).
Комментариев нет:
Отправить комментарий