Как decimal хранится в памяти

Обновлено: 05.07.2024

Знаковые дробные числа с сохранением точности операций сложения, умножения и вычитания. Для деления осуществляется отбрасывание (не округление) знаков, не попадающих в младший десятичный разряд.

Параметры

  • P - precision. Значение из диапазона [ 1 : 76 ]. Определяет, сколько десятичных знаков (с учетом дробной части) может содержать число.
  • S - scale. Значение из диапазона [ 0 : P ]. Определяет, сколько десятичных знаков содержится в дробной части числа.

Диапазоны Decimal

Например, Decimal32(4) содержит числа от -99999.9999 до 99999.9999 c шагом 0.0001.

Внутреннее представление

Операции и типы результата

Результат операции между двумя Decimal расширяется до большего типа (независимо от порядка аргументов).

Для размера дробной части (scale) результата действуют следующие правила:

  • сложение, вычитание: S = max(S1, S2).
  • умножение: S = S1 + S2.
  • деление: S = S1.

При операциях между Decimal и целыми числами результатом является Decimal, аналогичный аргументу.

Операции между Decimal и Float32/64 не определены. Для осуществления таких операций нужно явно привести один из аргументов функциями: toDecimal32, toDecimal64, toDecimal128, или toFloat32, toFloat64. Это сделано из двух соображений. Во-первых, результат операции будет с потерей точности. Во-вторых, преобразование типа - дорогая операция, из-за ее наличия пользовательский запрос может работать в несколько раз дольше.

Часть функций над Decimal возвращают Float64 (например, var, stddev). Для некоторых из них промежуточные операции проходят в Decimal.
Для таких функций результат над одинаковыми данными во Float64 и Decimal может отличаться, несмотря на одинаковый тип результата.

Проверка переполнений

При выполнении операций над типом Decimal могут происходить целочисленные переполнения. Лишняя дробная часть отбрасывается (не округляется). Лишняя целочисленная часть приводит к исключению.

Проверка переполнения приводит к замедлению операций. При уверенности, что типа результата хватит для его записи проверку переполнения можно отключить настройкой decimal_check_overflow. В этом случае при переполнении вернется неверное значение:

Переполнения происходят не только на арифметических операциях, но и на операциях сравнения. Отключать проверку стоит только при полной уверенности в корректности результата:

Подпишитесь на мой телеграм-канал, там я пишу о дотнете и веб-разработке.

Поехали

В этой статье я разбираю, как Double хранится в памяти, но это не самостоятельная статья, в том плане, что вряд ли вы сможете разобраться с этой темой, прочитав одну её, поэтому я порекомендую ссылки на другие статьи (хотя и их тоже не назовёшь идеальными).

Также в процессе подготовки этого материала я нашёл статью на русском: Взгляд со стороны: Стандарт IEEE754 - она в целом о стандарте хранения чисел IEEE754, статья основательная, но довольно абстрактная и её не назовёшь лёгким, доступным материалом.

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

Кратко о том как Double хранится в памяти

Double занимает в памяти 8 байт или 64 разряда в двоичном представлении. Старший разряд хранит знак числа - 0 обозначает положительное число, а 1 отрицательное. 11 следующих разрядов занимает часть, которую называют экспонента. Оставшиеся 52 - часть называемая мантисса. Комбинация этих трёх компонент в виде: знак * мантисса * 2 ^ экспонента , с небольшой предварительной манипуляцией над этими компонентами и будет представлять хранимое число. Есть разные классы хранимых чисел: нормализованные, субнормальные, бесконечность и Nan - они отличаются тем, как именно из хранимых компонент получается итоговое число.

Как можно посмотреть байтовое представление Double

Класс BitConverter позволяет получить байтовое представление базовых типов или наоборот преобразовать байтовое представление в базовый тип.

Double занимает 8 байтов в памяти и метод GetBytes возвращает нам массив из 8 элементов - всё сходится.

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

А можно воспользоваться методом DoubleToInt64Bits - он возвращает 64 битное целое число, которое в двоичном виде соответствует байтовому представлению числа типа double.

Для разобра удобнее сразу разделить число на его основные составляющие: мантиссу, экспоненту и знак.

Как получаются нормализованные числа

Нормализованное число - это, по сути, способ кодирования в мантиссе и экспоненте чисел представленных типом Double и лежащих в определённом числовом диапазоне (всех кроме специальных и очень маленьких). Формула для получения итогового числа: М * 2 ^ е , где M - мантисса, а e - экспонента, но чтобы получить Мантиссу и Экспоненту из данных непосредственно хранимых в Double, нужно проделать некоторые манипуляции над ними.

В Double для мантиссы отводятся 52 разряда, ещё один старший разряд в нормализованных числах подразумевается и он всегда равен 1. Чтобы из мантиссы хранимой в Double получить настоящую мантиссу, которую нужно умножать на экспоненту, нужно добавить к ней ещё один разряд заполненный 1 и разделить на 2 в степени 52.

Например для числа 1.0d хранимая мантисса (мы её получили в примере выше): 0000000000000000000000000000000000000000000000000000 - это 52 разряда заполненные нулями, при добавлении ещё одного подразумеваемого разряда заполненного единицей получается: 10000000000000000000000000000000000000000000000000000 - это уже 53 разряда с единицей в старшем разряде, при делении этого числа на 2^52 результат будет 1.0000000000000000000000000000000000000000000000000000 в двоичной системе исчисления (и в данном случае, точно такой же в десятичной - 1.0) - при делении на 2^52 мы двигаем двоичное число вправо на 52 разряда за точку целого числа в итоге мантисса это всегда число вида: 1.xxx. - в двоичной системе исчисления, где “xxx…” часть хранится в мантиссе Double. При переводе в десятичную систему исчисления получается, что мантисса всегда лежит в диапазоне: 1 <= M < 2

Теперь поговорим о том, как из хранимой экспоненты, получить настоящую экспоненту.

Для хранения экспоненты в Double отводится 11 разрядов - 11 разрядов дают нам 2^11 = 2048 числа с учётом 0 (то есть наибольшее хранимое число это 2047), которые мы можем использовать в качестве экспоненты. Но так как мантисса у нас всегда лежит в диапазоне от 1 до 2, а с помощью нормализованных чисел мы представляем как числа по модулю и большие 2 и меньшие 1, то нам необходимо в этих 11 разрядах хранить также отрицательные экспоненты, которые позволят получить числа меньше 1. Для этого подразумевается, что хранимая экспонента - это сдвиг относительно числа 1023. Примеры:

  1. Если в экспоненте Double хранится число 1023, то значит реальная экспонента: 1023 - 1023 = 0 .
  2. Если в экспоненте Double хранится число 1024, то значит реальная экспонента: 1024 - 1023 = 1 .
  3. Если в экспоненте Double хранится число 1022, то значит реальная экспонента: 1022 - 1023 = -1 .
  4. Если в экспоненте Double хранится число 0, то значит реальная экспонента… А вот так нельзя, 0 зарезервированное число и в нормализованных числах хранимая экспонента никогда не будет 0 - наименьшая хранимая экспонента - это 1, а значит наименьшая реальная экспонента 1 - 1023= -1022 . Но это только в нормализованных числах, если экспонента всё-таки равна 0, то значит кодируемое число либо 0, либо субнормальное число.
  5. Если в экспоненте Double хранится число 2047 (максимальное возможное число для 11 разрядов), то значит реальная экспонента… А вот так тоже нельзя, 2047 тоже зарезервированное число - наибольшая хранимая экспонента нормализованного числа - это 2046, а значит наибольшая реальная экспонента 2046 - 1023 = 1023 . Если вы всё-же видите в экспоненте 2047, то значит кодируемое число либо Nan, либо Infinity.

Теперь мы знаем как получить мантиссу и экспоненту из тех данных, что хранит Double. И мы можем посчитать самое большое и самое маленькое число, которое может хранится в нормализованном числе.

Самое большое число:

  • Самая большая реальная мантисса: 1.11111111 11111111 11111111 11111111 11111111 11111111 1111 или в десятичном виде 9 007 199 254 740 991 (53 разряда заполненные единицами) / 2^52 = 1.9999999999999997779553950749687
  • Самая большая реальная экспонента: 1022
  • Самое большое хранимое нормализованное число по нашим расчётам: 1.9999999999999997779553950749687 * 2^1022 = 1,797693134862315708145274237317e+308‬
  • И по официальным данным: 1.7976931348623157e+308

И это в принципе самое больше число, которое может хранится в Double, так как другие классы чисел не позволяют хранить числа большие, чем может хранить нормализованное число.

Самое маленькое положительное число:

  • Самая маленькая реальная мантисса: 1.0000000000000000000000000000000000000000000000000000
  • Самая маленькая реальная экспонента: -1022
  • Самое маленькое позитивное нормализованное число по нашим расчётам: 1.0 * 2^-1022 = 2,2250738585072013830902327173324e-308
  • И по официальным данным: 2.2250738585072010e-308

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

Любое нормализованное число из выше определённого диапазона нормализованных чисел может быть сделано отрицательным с помощью изменения старшего разряда в единицу в 64 разрядах типа данных Double.

Разберём некоторые нормализованные числа

Я написал сниппет, с помощью которого можно посмотреть разбор любого нормализованного числа:

Реальная экспонента - 0, так как само число уже лежит в диапазоне от 1 до 2, поэтому можно сразу же сохранить мантиссу, откинув целую 1.

Отличается от 1.0d только знаком в старшем разряде Double.

Число тоже лежит в диапазоне между 1 и 2, поэтому реальная экспонента будет 0.

Попробуем сами получить хранимую мантиссу:

1.1 * 2^52 = 4 953 959 590 107 545.6‬ округляем до 4 953 959 590 107 546 в двоичном виде: 1 0001 10011001 10011001 10011001 10011001 10011001 10011010 отбрасываем старший разряд и получаем искомое 0001100110011001100110011001100110011001100110011010

Попробуем сами получить хранимую мантиссу и экспоненту:

Сначала нам нужно привести число к виду: М * 2^e - так чтобы M лежала между 1 и 2. Это будет: 1,9073486328125 * 2^19 .

Теперь нам известна реальная экспонента - 19, для того, чтобы получить хранимую нужно к 1023 прибавить 19 итого получается: 1042 - это хранимая экспонента.

Нам также известна реальная мантисса - это 1,9073486328125 . Для того, чтобы получить хранимую мантиссу умножим её на 2^52: 1,9073486328125 * 2^52 = 8 589 934 592 000 000 . В двоичном виде: 1 1110 1000 0100 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 . Отбросим старший разряд и получим хранимую мантиссу: 1110 1000 0100 1000 0000 0000 0000 0000 0000 0000 0000 0000 0000 .

Попробуем сами получить хранимую мантиссу и экспоненту:

Сначала нам нужно привести число к виду: М * 2^e - так чтобы M лежала между 1 и 2. Это будет: 1,6 * 2^-4 .

Теперь нам известна реальная экспонента - это -4, для того, чтобы получить хранимую нужно к 1023 прибавить -4 итого получается: 1019 - это хранимая экспонента.

Нам также известна реальная мантисса - это 1,6 . Для того, чтобы получить хранимую мантиссу умножим её на 2^52: 1,6 * 2^52 = 7 205 759 403 792 793.6‬ после округления 7 205 759 403 792 794 . В двоичном виде: 1 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010‬ . Отбросим старший разряд и получим хранимую мантиссу: 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1010‬ .

Подведём итоги

Устройство вещественных чисел - 1

Привет! В сегодняшней лекции расскажем о числах в Java, а конкретно — о вещественных числах. Без паники! :) Никаких математических сложностей в лекции не будет. Будем говорить о вещественных числах исключительно с нашей, «программистской» точки зрения. Итак, что же такое «вещественные числа»? Вещественные числа — это числа, у которых есть дробная часть (она может быть нулевой). Они могут быть положительными или отрицательными. Вот несколько примеров: 15 56.22 0.0 1242342343445246 -232336.11 Как же устроено вещественное число? Достаточно просто: оно состоит из целой части, дробной части и знака. У положительных чисел знак обычно не указывают явно, а у отрицательных указывают. Ранее мы подробно разобрали, какие операции над числами можно совершать в Java. Среди них было много стандартных математических операций — сложение, вычитание и т. д. Было и кое-что новое для тебя: например, остаток от деления. Но как именно устроена работа с числами внутри компьютера? В каком виде они хранятся в памяти?

Хранение вещественных чисел в памяти

Думаю, для тебя не станет открытием, что числа бывают большими и маленькими :) Их можно сравнивать друг с другом. Например, число 100 меньше числа 423324. Влияет ли это на работу компьютера и нашей программы? На самом деле — да . Каждое число представлено в Java определенным диапазоном значений :
ТипРазмер в памяти (бит)Диапазон значений
byte 8 битот -128 до 127
short 16 битот -32768 до 32767
char 16 битбеззнаковое целое число, которое преставляет собой символ UTF-16 (буквы и цифры)
int 32 битаот -2147483648 до 2147483647
long 64 битаот -9223372036854775808 до 9223372036854775807
float 32 битаот 2 -149 до (2-2 -23 )*2 127
double 64 битаот 2 -1074 до (2-2 -52 )*2 1023
Сегодня поговорим именно о последних двух типах — float и double . Оба выполняют одну и ту же задачу — представляют дробные числа. Их еще очень часто называют « числа с плавающей точкой» . Запомни этот термин на будущее :) Например, число 2.3333 или 134.1212121212. Довольно странно. Ведь получается, нет никакой разницы между этими двумя типами, раз они выполняют одну и ту же задачу? Но разница есть. Обрати внимание на столбец «размер в памяти» в таблице выше. Все числа (да и не только числа — вообще вся информация) хранится в памяти компьютера в виде битов. Бит — это самая маленькая единица измерения информации. Она довольно проста. Любой бит равен или 0, или 1. Да и само слово « bit » происходит от английского « binary digit » — двоичное число. Думаю, ты наверняка слышал о существовании двоичной системы счисления в математике. Любое привычное нам десятичное число можно представить в виде набора единиц и нулей. Например, число 584.32 в двоичной системе будет выглядеть так: 100100100001010001111 . Каждые единица и ноль в этом числе являются отдельным битом. Теперь тебе должна быть более понятна разница между типами данных. Например, если мы создаем число типа float , в нашем распоряжении есть всего 32 бита. При создании числа float именно столько места будет выделено для него в памяти компьютера. Если же мы хотим создать число 123456789.65656565656565, в двоичном виде оно будет выглядеть так: 11101011011110011010001010110101000000 . Оно состоит из 38 единиц и нулей, то есть для его хранения в памяти нужно 38 бит. В тип float это число просто не «влезет»! Поэтому число 123456789 можно представить в виде типа double . Для его хранения выделяется целых 64 бита: это нам подходит! Разумеется, и диапазон значений тоже будет подходящим. Для удобства ты можешь представлять число как маленький ящик с ячейками. Если ячеек хватает для хранения каждого бита, значит, тип данных выбран правильно :) Устройство вещественных чисел - 2
Разумеется, разное количество выделяемой памяти влияет и на само число. Обрати внимание, что у типов float и double отличается диапазон значений. Что это означает на практике? Число double может выразить большую точность, чем число float . У 32-битных чисел с плавающей точкой (в Java это как раз тип float ) точность составляет примерно 24 бита, то есть около 7 знаков после запятой. А у 64-битных чисел (в Java это тип double ) — точность примерно 53 бита, то есть примерно 16 знаков после запятой. Вот пример, который хорошо демонстрирует эту разницу: Что мы должны получить здесь в качестве результата? Казалось бы, все довольно просто. У нас есть число 0.0, и мы 7 раз подряд прибавляем к нему 0.1111111111111111. В итоге должно получиться 0.7777777777777777. Но мы создали число float . Его размер ограничен 32 битами и, как мы сказали ранее, он способен отобразить число примерно до 7 знака после запятой. Поэтому в итоге результат, который мы получим в консоли, будет отличаться от того, что мы ожидали: Число как будто было «обрезано». Ты уже знаешь как хранятся данные в памяти — в виде битов, поэтому тебя не должно это удивлять. Понятно, почему это произошло: результат 0.7777777777777777 просто не влез в выделенные нам 32 бита, поэтому и был обрезан так, чтобы поместиться в переменную типа float :) Мы можем изменить тип переменной на double в нашем примере, и тогда итоговый результат не будет обрезан: Здесь уже 16 знаков после запятой, результат «уместился» в 64 бита. Кстати, возможно ты заметил, что в обоих случаях результаты получились не совсем корректными? Подсчет был произведен с небольшими ошибками. О причинах этого мы поговорим ниже :) Теперь скажем пару слов о том, как можно сравнить числа между собой.

Сравнение вещественных чисел

Устройство вещественных чисел - 3

Мы частично уже затрагивали этот вопрос в прошлой лекции, когда говорили об операциях сравнения. Такие операции как > , < , >= , <= повторно разбирать мы не будем. Вместо этого рассмотрим более интересный пример: Как ты думаешь, какое число будет выведено на экран? Логичным ответом был бы ответ: число 1. Мы начинаем отсчет с числа 0.0 и последовательно прибавляем к нему 0.1 десять раз подряд. Вроде все правильно, должна получиться единица. Попробуй запустить этот код, и ответ сильно тебя удивит :) Вывод в консоль: Но почему в таком простом примере возникла ошибка? О_о Тут бы даже пятиклассник с легкостью верно ответил, но программа на Java выдала неточный результат. «Неточный» тут более подходящее слово, чем «неправильный». Мы все-таки получили очень близкое к единице число, а не просто какое-то рандомное значение :) Оно отличается от правильного буквально на миллиметр. Но почему? Возможно, это просто разовая ошибка. Может, комп заглючил? Попробуем написать другой пример. Вывод в консоль: Так, дело явно не в глюках компа :) Что происходит? Подобные ошибки связаны с тем, как числа представлены в двоичном виде в памяти компьютера. Дело в том, что в двоичной системе невозможно точно представить число 0,1 . В десятичной системе, кстати, тоже есть подобная проблема: в ней нельзя правильно представить дроби (и вместо ⅓ мы получим 0.33333333333333…, что тоже не совсем правильный результат). Казалось бы, мелочь: при таких подсчетах разница может быть в одну стотысячную часть (0,00001) или даже меньше. Но что, если от этого сравнения будет зависеть весь результат работы твоей Очень Серьезной Программы? Мы явно ожидали, что два числа будут равны, но из-за особенностей внутреннего устройства памяти мы отменили запуск ракеты. Раз так, нам нужно определиться, как же все-таки сравнить два числа с плавающей точкой, чтобы результат сравнения был более. эммм. предсказуемым. Итак, правило №1 при сравнении вещественных чисел мы уже усвоили: никогда не используй == при сравнении чисел с плавающей точкой. Ок, плохих примеров, думаю, достаточно :) Давай рассмотрим хороший пример! Здесь мы по сути делаем то же самое, но меняем способ сравнения чисел. У нас есть специальное «пороговое» число — 0.0001, одна десятитысячная. Оно может быть и другим. Это зависит от того, насколько точное сравнение тебе нужно в конкретном случае. Можно сделать его и больше, и меньше. С помощью метода Math.abs() мы получаем модуль числа. Модуль — это значение числа независимо от знака. Например, у чисел -5 и 5 модуль будет одинаковым и равен 5. Мы вычитаем второе число из первого, и если полученный результат, независимо от знака, будет меньше того порога, который мы установили, значит наши числа равны. Во всяком случае, они равны до той степени точности, которую мы установили с помощью нашего «порогового числа» , то есть как минимум они равны вплоть до одной десятитысячной. Такой способ сравнения избавит тебя от неожиданного поведения, которое мы увидели в случае с == . Еще один хороший способ сравнения вещественных чисел — использовать специальный класс BigDecimal . Этот класс специально был создан для хранения очень больших чисел с дробной частью. В отличие от double и float , при использовании BigDecimal сложение, вычитание и прочие математические операции выполняются не с помощью операторов ( +- и т.д.), а с помощью методов. Вот как это будет выглядеть в нашем случае: Какой же вывод в консоль мы получим? Мы получили ровно тот результат, на который рассчитывали. И обрати внимание, насколько точными получились наши числа, и сколько знаков после запятой в них уместилось! Гораздо больше, чем во float и даже в double ! Запомни класс BigDecimal на будущее, он тебе обязательно пригодится :) Фух! Лекция получилась немаленькая, но ты справился: молодец! :) Увидимся на следующем занятии, будущий программист!

Содержание статьи:

Введение

  1. Типы значений — это тип данных, содержащий значения данных в собственном пространстве памяти. Хранятся в стеке, а потому их можно быстро создавать и удалять.
  2. Ссылочные типы — хранят ссылку на значения и указывают на другую ячейку памяти, в которой хранятся данные. Хранятся в управляемой куче.

Подразделение типов значения

Подразделение ссылочных типов

  • Object ;
  • String ;
  • Class ;
  • Interface ;
  • Delegate .

Использование суффиксов float, decimal, double

У некоторых числовых типов имеются суффиксы, позволяющие записывать значение типа в переменную.

Числовые типы с плавающей запятой — это действительные числа. Они принадлежат к категории Типы значения. Это простые типы, которые могут быть инициализированы литералами. Они поддерживают операторы сравнения, равенства и арифметические операторы.

Все типы с плавающей запятой имеют свои константы MaxValue и MinValue . Типы float и double в дополнение имеют константы, которые обозначают нечисловые и бесконечные значения.

Литералы

Тип определяется суффиксом:

  • double имеет суффиксы D или d ;
  • float имеет суффиксы F или f ;
  • decimal имеет суффиксы M или m .

Decimal и float

Decimal и float используются для хранения числовых значений:

Класс convert создан для того, чтобы преобразовывать широкий спектр типов. С его помощью можно преобразовывать в десятичное число больше типов. Метод Convert.ToDecimal используется для преобразования строкового представления числа в эквивалентное десятичное число с информацией о форматировании.

Вывод: преобразование в десятичное значение указанных строк:

123456789, 12345.6789, 123456789.0123.

Метод Decimal.ToInt32() создан для преобразования decimal значения в эквивалентное 32-разрядное целое число со знаком.

Вывод: Int32: 2147483647 и Int32: 21458565 .

Вывод: округленное значение 184467440737096.

2 Round(Decimal, Int32) Method — округление значения Decimal до указанного количества десятичных знаков;

Вывод: округленное значение 7922816251426433759354,3950 .

3 Round(Decimal, Int32, MidpointRounding) Method ;

4 Round(Decimal, MidpointRounding) Method .

Название встроенного типа (столбец Ключевое слово) — и есть сокращенное обозначение системного типа (столбец Системный тип).

Неявная типизация

Чтобы указать призвольный тип переменных, можно использовать неявную типизацию. Для этого используется ключевое слово var .

Var используется вместо названия типа данных. Присвоенное значение позволяет компилятору выводить тип данных. В примере Console.WriteLine(c.GetType().ToString()) ; определяет тип переменной с . Целочисленные значения по умолчанию рассматриваются как int , поэтому переменная с имеет тип System.Int32 .

Но такие переменные имеют свои ограничения:

1 Нельзя определить неявную переменную и сразу ее инициализировать.

2 Неявная переменная не может иметь значение null , в этом случае компилятор не определит автоматически тип данных.

Для больших дробных чисел проще всего использовать тип double . Decimal имеет большую разрядность в сравнении с double , но double хранит большее значение.

После запятой decimal может иметь до 28 цифр, тогда как double — до 16. Тем не менее double широко используется в математических вычислениях, а decimal — в финансовых.

Таблица различий между double и decimal

Типы данных могут быть простыми и сложными. Сложные типы чаще всего данные структурируют, а у простых значения данных неделимы. Любой язык программирования имеет систему встроенных типов данных, на их основе можно создавать свои производные.

  • типы значений (входит большинство встроенных типов в т.ч. пользовательские) — для их создания применяется ключевое слово struct ;
  • ссылочные типы — для их создания применяется ключевое слово class .

Highload нужны авторы технических текстов. Вы наш человек, если разбираетесь в разработке, знаете языки программирования и умеете просто писать о сложном!
Откликнуться на вакансию можно здесь .

Читайте также: