Почему разные языки по-разному отображают число 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
Ответ
Здесь в каждой среде/языке два преобразования:
Из константы в исходном коде в объект в памяти
Печать этого объекта памяти выбранным способом.
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 как средство печати переменных в С
В данном случае, так как число является степенью двойки, то легко найти его IEEE 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
int main(void)
{
volatile double x = 9223372036854775808.0;
printf("%f
", nextafter(x, DBL_MIN));
printf("%f
", x);
printf("%f
", 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.1000000000000000055511151231257827021181583404541015625 ( 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'
Комментариев нет:
Отправить комментарий