Страницы

Поиск по вопросам

воскресенье, 24 ноября 2019 г.

Отображение числа 9223372036854775807


Почему разные языки по-разному отображают число 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. Заставить вывести сразу в правильной форме не получилось :( Вывод. Все, кроме шарпа вывели правильно. Абсолютно правильно. Просто, по исторически (и видимо патентным/велосипедным правилам) используют разные алгоритмы предобразования. Шарп выделился, но думаю, что на то есть исторические причины (вплоть до оптимизации по скорости или просто человеческая ошибка).

Комментариев нет:

Отправить комментарий