Comments 46
Более-менее понятно. Интересное следствие: из такой формы следует, что уже у сравнительно небольших чисел, порядка 2^24, падает точность представления даже целой части (потому что мантисса делит всегда на 2^23 элемента, и если элементов между двумя точками будет больше, то каждому второму не достанется своего значения):
#include <iostream>
using namespace std;
int main() {
cout << (16777215.f == 16777216.f) << endl;
cout << (16777216.f == 16777217.f) << endl;
}
Выведет 0 и 1, т.е. во float-представлении 16777216 равняется 16777217. Мне кажется, этот пример даже интереснее известного многим 0.1+0.2.
Интересно было бы почитать в доступной форме, как производятся математические операции с float-числами в таком виде и как можно эффективно перевести число из обычной строки во float.
float floatValue = 1F / 3F;
double doubleValue = 1D / 3D;
decimal decimalValue = 1M / 3M;
for (int i = 0; i <= 6; i++) {
float floatResult = 0;
double doubleResult = 0;
decimal decimalResult = 0;
int times = Convert.ToInt32(3*Math.Pow(10,i));
for (int j = 1; j <= times; j++) {
floatResult += floatValue;
doubleResult += doubleValue;
decimalResult += decimalValue;
}
Console.WriteLine("sum 1/3 times: {0}" , times);
Console.WriteLine("flt = {0}", floatResult);
Console.WriteLine("dbl = {0}", doubleResult);
Console.WriteLine("dec = {0}", decimalResult);
Console.WriteLine();
}
Console.WriteLine("flt = {0}", floatValue*3000000);
Console.WriteLine("dbl = {0}", doubleValue*3000000);
Console.WriteLine("dec = {0}", decimalValue*3000000);
sum 1/3 times: 3
flt = 1
dbl = 1
dec = 0,9999999999999999999999999999
sum 1/3 times: 30
flt = 9,999999
dbl = 10
dec = 9,999999999999999999999999997
sum 1/3 times: 300
flt = 100,0002
dbl = 99,9999999999997
dec = 99,99999999999999999999999972
sum 1/3 times: 3000
flt = 999,9764
dbl = 1000,00000000004
dec = 999,9999999999999999999999720
sum 1/3 times: 30000
flt = 9999,832
dbl = 10000,0000000003
dec = 9999,999999999999999999997203
sum 1/3 times: 300000
flt = 100165,5
dbl = 99999,9999996892
dec = 99999,99999999999999999972026
sum 1/3 times: 3000000
flt = 976144,6
dbl = 1000000,00004317
dec = 999999,9999999999999999720256
flt = 1000000
dbl = 1000000
dec = 999999,9999999999999999999999
Видно, что при сложении 1/3 три раза float и double дают более точный результат, чем decimal. Правда при большом количестве сложений decimal точнее. С другой стороны, если не складывать 1/3 три миллиона раз, а сразу умножить, то decimal менее точный.
Всё это как-то связано с 60-ричной системой счисления. Я думаю, что при работе с долями (весами) или с географическими координатами арифметика с плавающей запятой может быть точнее. Если кто-то может рассказать подробней было бы интересно.
Там не в знаках считается, а в битах. В статье сказано, что мантисса составляет 23 бита, так что максимальная достижимая точность на целых числах будет, когда между 2^E и 2^(E+1) (отбросим для простоты -127) будет 2^23 чисел, тогда каждому числу можно противопоставить одно значение мантиссы. Если же этих чисел больше, то можно найти такое число, которому уже не сопоставишь своё выделенное значение мантиссы, вот я для примера выбрал участок, где 2^24 чисел, т.е. в два раза больше. Поэтому у чётных чисел есть своё значение мантиссы, а нечётным уже не хватает, и происходит «округление».
Вообще то исходно архитектура процессоров разрабатывалась для выполнения арифметических операцуий с плавающей запятой — в частности, на таких системах я учился в свое время.
Но изобретение велосипедов сопутствует развитию — поэтому не удивляюсь
К тому же тема не раскрыта до конца, потому что, кроме описанной здесь нормализованной формы, есть также денормализованная форма и разные нечисла, типа NaN и Inf.
Лично я односимвольные регистрочувствительные имена переменных/классов без обильных комментариев воспринимаю достаточно посредственно. Так что мне эта статья была небесполезна.
С картинкой получилось как-то более явно увидеть грабли, спрятанные в арифметике с плавающей точкой.
Я например смог понять как с ней работать только после прочтения этой статьи, где было всё объяснено человеческим языком и показано на конкретном примере. Теперь смотря на формулу, всё становится на свои места и понятно, почему она такая и как с ней работать. Но это требует не математического, а «человеческого» объяснения.
Другой пример — производная функции
Производная функции — предел отношения приращения функции к приращению независимой переменной при стремлении последнего к нулю (если этот предел существует)
Определение точно? Точно. Понятно? Нет. В ответ на это определение надо выяснять, что теперь делать с этим произведением и зачем оно вообще надо. Если же сказать, что производная функции — это скорость изменения функции в конкретной точке, становится понятно, но при этом не точно. Аналогичная история происходит в этой статье. Формула нужна для точного понимания, а картинки для первичного понимания сути.
От статьи на хабря я обычно жду полноты, то есть, хорошо было бы написать про денормализованные числа, про нечисла и правила их обработки, про разные варианты, предусмотренные стандартом, включая децимальный формат, а также 128- и 256-битные числа, про 80-битный формат, который был принят в x86 изначально, про разные исторические и необычные форматы (типа модульного логарифмического). Вот это было бы интересно.
мантиса — номер шага
в статье не увидел, почему записывают
3,14 = 1,57 *2 ( 2 в степени 1 равно 2, то есть экспонента 128-127 = 1)
вместо
3,14 = 3,14*1 (2 в степени 0 равно 1, то есть экспонента 127-127 = 0)
А есть процессоры или теории с другим представлением чисел с запятой? Вообще описанное представление оптимально или исрользуется по-традиции?
В языке C значения с плавающей запятой — это 32-битные контейнеры, соответствующие стандарту IEEE 754
неправда:
типов с плавающей запятой несколько
они платформозависимы
IEEE 754 в С99 носит рекомендательный характер, а С11 ссылается на более поздние стандарты
sites.math.washington.edu/~morrow/336_12/papers/ben.pdf
Если простая матзапись числа с плавающей запятой непонятна?
(-1)^S * 0.S * 10 ^ (E-127)
Такое представление отличается тем, что приведение в него неоднозначно: 0.001 можно записать как 0.001*10^0 или как 0.01*10^-1 или как 0.1*10^-10
При этом приведение к форме (-1)^S * 1.M * 10 ^ (E-127) однозначно, потому что ведущая единица в числе только одна.
(-1)^S * 1.M * 10 ^ (E-127)
Чем это хуже чем:
(-1)^S * 1.M * 2 ^ (E-127)
?
С основанием 10 всё понимается в разы легче, по крайней мере для нас, 10-чных людей.
Что там можно представить — вопрос вторичный, т.к. это решаемо в любом случае. Старшую единицу сделали неявной просто потому, что оказалось можно сэкономить 1 бит, вряд ли кто-то специально к этому стремился заранее.
А двоичные порядки потому что процессор вообще обычно двоичный, и создавать какую-то специальную недвоичную логику ради малопонятных целей резона нет. А там где надо вывести в понятном виде человеку — двоичные порядки легко конвертируются в десятичные соответствующей программой.
откуда тут взялось что 2^1 →E=128 ??? как объяснить не привлекая формулу?
Представление чисел с плавающей точкой имеет погрешность. Число пи имеет погрешность. 2*pi имеет еще большую погрешность, 3*pi еще больше, и так далее. Каким должно быть N в формуле N*pi, чтобы cos(N*pi) посчитанный на ieee-754 дал 0 или 1? (N в этой задаче — целое, разумеется)
Number.MAX_SAFE_INTEGER
получил число: 9007199254740991.Это двойная точность = 64 бит, знак 1, мантиса 52, експонента 11.
Если к этому числу, например, добавить 1н, то будет происходить потеря точности.
А в пределах до этого числа, целые числа не теряют точность.
Всё что влезет в мантису, не теряет точность, даже дробные значения.
Но много чего не представимо в 52 бита. Например двоичный результат от 0.1+0.2.
Значение усекается до 52 бит и следовательно точность теряется.
Кстати (0.1+0.2)-0.3 = 5.551115123125783e-17
Наглядное объяснение чисел с плавающей запятой