float и double представляются в формате IEEE-754. Это формат предложенный Intel в 70 годах 20 века. Число в таком формате представлено знаком(sign), порядком(exponent) и мантиссой(mantissa). Float в таком формате кодируется семью битам порядка и 23 битами мантиссы.
Например, число 12.5 можно представить в экспоненциальном виде несколькоми способами, 12.5 = 0.125e+2 , 12.5 = 1.25e+1. 12.5 = 125e-1. Первое из трех представлений называется нормализованным, потому что мантисса попадает в интервал 0.1 < 0.125 < 1. Это признак нормализованности.
Если расчеты проводятся в двоичной системе, то первая цифра мантиссы всегда будет 1, поэтому можно хранить эту цифру в памяти, и дописывать каждый раз, когда производятся расчеты. Мы будем считать, что эта единица добавляется как целая часть перед мантиссой как дробной частью, пример : 12.5 = 1100.1 = 1.1001e+3 . Мантисса = 1.1001, в память будет записано только 1001, единица перед запятой опускается. Это дает возможность использовать более широкий диапазон значения мантиссы( не от 0.1 до 1, а от 1 до 2). Если я правильно понимаю, то тем самым увеличили стартовое значение float (было 0.1 * 2^-126, стало 1*2^-126).
Приведу пример с представлением числа 12.5 в ieee-754:
number = 12.5
binaryNumber = 1100.1
otherFormatOfBinaryNumber = 1.1001e+3
sign = 0; // it's positive number
exponent = 127 + 3 = 130 // adding 127(2^7-1) ;
mantissa = 0.1001;
sign exponent mantissa
|0| |1|0|0|0|0|0|1|0| |1|0|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|
Прием с прибавлением 128 нужен, чтобы не хранить в exponent'е отрицательное число, тем самым не теряя один разряд на знак числа
Порядок перевода такой:
- Представляем число в двоичном виде (это можно сделать не всегда, если число не раскладывается по степеням двойки, то заканчиваем переводить, когда его длина превысит 22 знака)
- Если число дробь - то сдвигаем точку так, чтобы слева осталась только одна единица. Это может быть сдвиг как вправо(если число меньше 2), так и влево (если число больше 2). Это и есть мантисса. Exponent = 127 + количество разрядов на которое передвинули точку.
- У нас есть знак, мантисса и порядок
Также ieee включает специальные соглашения для +0,-0, +infinity, -infinity, NaN:
- +0 - экспонента = 0, sign = 0, мантисса = 0
- -0 - экспонента = 0, sign = 1, мантисса = 1
- +infinity - экспонента = 255 (все биты экспоненты заполнены единицами), sign = 0, мантисса = 0
- -infinity - экспонента = 255 (все биты экспоненты заполнены единицами), sign = 1, мантисса = 0
- NaN - экспонента = 255, sign - любой, мантисса != 0
в c# эти числа можно получить так:
В c# +0,-0, никак явно не выражено, но вот пример, когда нужны +0 и -0:
var x = float.PositiveInfinity;
var f = 1f/(1f/x); // +Infinity, на промежуточном этапе 1f/x = +0
x = float.NegativeInfinity;
f = 1f/(1f/x); // -Infinity, на промежуточном этапе 1f/x = -0
// Если бы +0 и -0 были неотличимы, тогда результат всегда бы был +Infinity
Еще один пример:
var positiveInfinity = float.PositiveInfinity // (0 / 1f) + float.PositiveInfinity; // +Infinity
var nan = float.PositiveInfinity // (0 / -1.0f) + float.PositiveInfinity; // NaN
У меня получилось, что время выполнение простого деления float'ов и float/positiveInfinity и float/negativeInfinity одинаково
Можно заметить, что мантисса принимает значения от 1 до (2 - 2^-23), а экспонента от -126 до 127 получаем, что в таком формате можно представить числа от 1*2^-126 до (2-2^-23)*2^127, в десятичном виде : от 1.175494e-38 до 3.4028235e+38.
Попробуем оценить ошибку в представлении числа(нужно в итоге оценить ее в мат. виде, пока этого нет) . Ошибка будет равна 0, когда число представляется в двоичном виде, если не представляется, то нужно оставить только 23 знака, а остальное отрезать. Далее, поскольку число представляется произведением мантиссы на экспоненту, то при большом значении экспоненты, точность будет теряться. Это отлично показано (тут (Подзаголовок "Другой способ объяснения))[https://habr.com/ru/post/337260/]. Получается, что чем ближе числа к 0 тем они точнее, проверим:
float w = 0.00001f; // 9.99999945E-6 = 0.0000099999945 // потеря намного меньше 0.00001
float ww = 100000001f; // 100000000 // потеря = 1
Поэтому float не подходит для таких вычислений, но , что более страшно, это то, что потенциально близкие числа при вычитании могут дать 0. [Тут очень хорошо это показано, в разделе про денормализованные числа][https://habr.com/ru/post/337260/].
Поэтому встал вопрос о том, как при тех же затратах памяти получить лучшую точность. Intel ввел понятия денормализованных чисел, тех у кого экспонента равна (0 - (2^7 - 2)) = 126, и приписываем спереди мантиссы 0., а не 1. Пример:
num = -1.40129846432e-45
sign = 1
exponent = 0
mantissa = 00000000000000000000001 (22 нуля, единица)
// В бинарном виде 0.00000000000000000000001e-126, это самый малелький float.
Оценим величины денормализованных float(тут оцениваются только положительные): min = 2^-126 * 2^-23 = 2^-149 = 1.40129846432e-45 max = 2^-126 * (2 - 2^24) = 1.17549421069e-38
Для double хранится 11 символов под экспоненту, 52 под мантиссу и 1 под знак. Минимальное значение экспоненты = 1 - (2^10-1) = 1 - 1023 = -1022, мантиссы = 1 Минимальное значение double = 2^(-1022) = 2.22507385850720187715587855858E-308 Максимальное значение экспоненты = 2^11 - 2 - (2^10-1) = 2^11 - 2^10 - 1 = 2^10 - 1 = 1023, мантиссы 2 - 2^-52 Максимальное значение doublе = 2^1023 *(2 - 2 ^-52) = 2 ^ 1024 - 2 ^ 971 = 1.797693e+308
Также существуют денормализованные double, для них значение экспоненты = 0 - (2^4 - 2) = -1022: Мининимальное значение мантиссы = 1, минимальное число в формате double = 2^(-1022) * 2^-52 = 2^(-74) Максимальное значение мантиссы = (1 - 2^-52) * 2 ^ -1022 = 2250738585072008890245868760859e-308
- Cложениe Оба числа приводятся к наибольшей экспоненте а далее мантиссы складываются, например:
// на последнем шаге нормализуем число
1) 1.01101e+2 + 1.010011e+3 = 0.1011011e+3 + 1.01001e+3 = 10.000000e+3 = 1.0e+4
// Пример с отрицательной экспонентой
2) 1.1e-1 + 1.1101e-3 = 1.1e-1 + 0.011101e-1 = 1.111101e-1
- вычитание Оба числа приводятся к наибольшей экспоненте и мантиссы вычитаются
1.0e-4 - 1.1e-2 = 0.01e-2 - 1.1e-2 = 1.01
- умножение Перемножаем мантиссы и складываем порядки, при необходимости нормализуем
// на последнем шаге нормализовали
1.11e+3 * 1.101e-1 = (111*1101)e(+3 + (-1)) = 10.110110e+2 = 1.0110110e+3;
- деление Делим мантиссы и вычитаем порядки, при необходимости нормализуем
// При делении не получается точное число, нужно взять только 23 знака для одинарной точности.
1.1e+1 / 1.111e-1 = (11/1111)e(+1-(-1)) = 0.00110011001100110011e+2 = 1.10011001100110011e-1
Как происходит сложение,вычитание, умножение,деление, возведение в квадрат, вычисление корня, округление процессором
Начнем со сложения, это может быть сложение положительных, отрицательных , положительного и отрицательного чисел.
Все операции аналогичны математическому сложению чисел с плавающей точкой рассмотренных в разделе выше. Рассмотрим машинную составляющую. Операции с отрицательными числами с плавающей точной происходят в дополнительном коде:
- При сложении в регистры помещаются мантиссы и порядки обоих чисел (используются 4 регистра) Я буду приводить примеры из (пособия)[http://window.edu.ru/resource/957/74957/files/A_d_p.pdf]. Пусть нам нужно сложить два числа X = 24, Y = -65, оба числа представлены в 8ичной системе счисления:
Разложим оба числа с экспоненциальном виде:
24 в 8ичной = 10100 в двоичной = 1.0100e+4 , exponent = 127 + 4
sign exponent mantissa
|0| |1|0|0|0|0|0|1|1| |0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|
-65 в 8ичной = -110101 в двоичной = -1.10101e+5, exponent = 127 + 5
sign exponent mantissa
|1| |1|0|0|0|0|1|0|0| |1|0|1|0|1|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|0|
Теперь поместим в регистры exponent'ы и мантиссы:
Под значение мантисс отводится 25битные регистры, хотя при хранении используется только 23 бита, добавленные 2 бита под знак и скрытую единицу(которую мы не храним, а добавляем только в момент вычислений).
Первое действие это выравнивание порядков в большую сторону. Почему в большую а не в меньшую? Потому что если выравнивать порядки в меньшую сторону может случиться переполнение:
x = 1.1e+1
y = 1.1e0
// Если выравнивать в меньшую:
x = x << 1 = 1e0
y = y = 1.1e0.
// в числе x произошло переполнение.
Я написал код частично имитирующий сложение чисел с плавающей точкой, с ним можно ознакомиться (тут)[https://github.com/brager17/FloatArithmetic/blob/master/FloatArithmetic/IE754Operations.cs]
Показать как происходит сравнение и выравние порядков, потом сложение дробных мантисс в доп.коде и нормализация, понять почему сложение мантисс производится по правилам сложение чисел с фиксированной точкой. Страница 52, там приведен пример как процессор производит сложение и вычитание :http://window.edu.ru/resource/957/74957/files/A_d_p.pdf
Тут кратко: http://csaa.ru/slozhenie-i-vychitanie-veshhestvennyh-chisel/