Comments 44

Более-менее понятно. Интересное следствие: из такой формы следует, что уже у сравнительно небольших чисел, порядка 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.

В свете вашего комментария стоит отметить, что в javascript все числа — это double. Там, конечно, диапазон целых значений, вычисляемых без погрешности, больше, но он заметно меньше, чем 2^64.

После некоторых трюков там можно оперировать 32-битными целыми числами

Есть ещё интересный пример:

1/3 + 1/3 + 1/3 = ?
Складываем 1/3 сначала 3 раза, затем 30 раз, 300 раз и т.д.:

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-ричной системой счисления. Я думаю, что при работе с долями (весами) или с географическими координатами арифметика с плавающей запятой может быть точнее. Если кто-то может рассказать подробней было бы интересно.

А вот это интересно, для работы бух софта это учитывается? Хотя там 1/3 не бывает в принципе.

UFO landed and left these words here

Там не в знаках считается, а в битах. В статье сказано, что мантисса составляет 23 бита, так что максимальная достижимая точность на целых числах будет, когда между 2^E и 2^(E+1) (отбросим для простоты -127) будет 2^23 чисел, тогда каждому числу можно противопоставить одно значение мантиссы. Если же этих чисел больше, то можно найти такое число, которому уже не сопоставишь своё выделенное значение мантиссы, вот я для примера выбрал участок, где 2^24 чисел, т.е. в два раза больше. Поэтому у чётных чисел есть своё значение мантиссы, а нечётным уже не хватает, и происходит «округление».

Вообще то исходно архитектура процессоров разрабатывалась для выполнения арифметических операцуий с плавающей запятой — в частности, на таких системах я учился в свое время.
Но изобретение велосипедов сопутствует развитию — поэтому не удивляюсь

операции с плавающей запятой присутствовали в компьютерах с самого начала. Их не было только в специализированных компьютерах и самых дешевых компьютерах общего назначения.
Они с самого начала были в «больших» компьютерах (предназначенных в первую очередь для учёных), но очень долго отсутствовали в ПК (предназначенных в первую очередь для секретарш).
Мне как раз кажется понятной формула, и кажутся непонятными все эти объяснения с картинками. Формула сразу даёт понимание того, как с этими числами работать, то есть как складывать, умножать и делить, пользуясь их бинарным представлением.
К тому же тема не раскрыта до конца, потому что, кроме описанной здесь нормализованной формы, есть также денормализованная форма и разные нечисла, типа NaN и Inf.
Я, конечно, могу ошибаться, но каждому — своё.
Лично я односимвольные регистрочувствительные имена переменных/классов без обильных комментариев воспринимаю достаточно посредственно. Так что мне эта статья была небесполезна.
С формулой понятно, как оперировать этими значениями.
С картинкой получилось как-то более явно увидеть грабли, спрятанные в арифметике с плавающей точкой.
Это вы увидели только первый слой граблей. А там внизу еще целая куча. И основная — поддержка точности. Точность теряется на всех этапах вычислений, так что конечный результат может не иметь ничего общего с действительностью, несмотря на то, что все используемые в расчетах формулы были правильными.
Формула ужасна, объяснение, а особенно картинка, отличные. Наверное, нужно быть математиком, чтобы сходу понять, как эти числа делить глядя только на формулу.
Проблема такой математической записи, что она понятна только тем кто понимает. Простите за каламбур. Математическая запись точна, отбрасывает всё лишнее, но обывателю в большинстве случаев вообще не понятна.
Я например смог понять как с ней работать только после прочтения этой статьи, где было всё объяснено человеческим языком и показано на конкретном примере. Теперь смотря на формулу, всё становится на свои места и понятно, почему она такая и как с ней работать. Но это требует не математического, а «человеческого» объяснения.

Другой пример — производная функции
Производная функции — предел отношения приращения функции к приращению независимой переменной при стремлении последнего к нулю (если этот предел существует)

Определение точно? Точно. Понятно? Нет. В ответ на это определение надо выяснять, что теперь делать с этим произведением и зачем оно вообще надо. Если же сказать, что производная функции — это скорость изменения функции в конкретной точке, становится понятно, но при этом не точно. Аналогичная история происходит в этой статье. Формула нужна для точного понимания, а картинки для первичного понимания сути.
Я с вами согласен в том смысле, что формула не даёт понимания, когда смотришь на неё в первый раз. Но для объяснения принципа представления вещественных чисел в формате IEEE 754 в нормализованной форме достаточно разобрать один-два примера.
От статьи на хабря я обычно жду полноты, то есть, хорошо было бы написать про денормализованные числа, про нечисла и правила их обработки, про разные варианты, предусмотренные стандартом, включая децимальный формат, а также 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)
в двоичном виде мантиса 1.M, то есть выигрывают 1н бит, а в десятичном может быть 1.M или 9.M
Нет окна [0, 1]. Есть только [1/2, 1), [1/4, 1/2) и так далее. 0 в нормализованном представлении отобразить нельзя, поэтому он искусственно принят как (E=0, M=0).
Не могу согласиться с утверждением, что у процессора PDP-11 не было модулей плавающей точки. Был FIS (Floating Instruction Set) и кое-где FPP (Floating Point Processor)
Статья доходчиво объясняет сложную для понимая вещь. Спасибо за перевод автору!
Но остался для меня один непонятный момент:

То есть значение 3,14 аппроксимируется как 3,1400001049041748046875.


Я не понял, как вычисляется вот эта часть 0,0000001049041748046875?
M = 2^23*0.57 = 8388608 *0.57 = 4781506,56
4781507 — 4781506,56 = 0.44
0.44*2/8388608 = 0,0000001049041748046875

А есть процессоры или теории с другим представлением чисел с запятой? Вообще описанное представление оптимально или исрользуется по-традиции?

В языке C значения с плавающей запятой — это 32-битные контейнеры, соответствующие стандарту IEEE 754

неправда:
типов с плавающей запятой несколько
они платформозависимы
IEEE 754 в С99 носит рекомендательный характер, а С11 ссылается на более поздние стандарты
Может нубский вопрос, а чем интересно такое представление отличается в плане скорости/точности?
(-1)^S * 0.S * 10 ^ (E-127)
Возможно, вы имели в виду: (-1)^S * 0.M * 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 бит, вряд ли кто-то специально к этому стремился заранее.
А двоичные порядки потому что процессор вообще обычно двоичный, и создавать какую-то специальную недвоичную логику ради малопонятных целей резона нет. А там где надо вывести в понятном виде человеку — двоичные порядки легко конвертируются в десятичные соответствующей программой.

Число 3,14 находится между степенями двойки 2 и 4, то есть окно числа с плавающей запятой должно начинаться с 2^1 →E=128 (см. формулу, где окно — это 2(E−127)).

откуда тут взялось что 2^1 →E=128 ??? как объяснить не привлекая формулу?
Не привлекая формулу, можно считать так:
Окно | Е

[1,2] | 127
[2,4] | 128
[4,8] | 129
Тут было бы уместно рассмотреть такую задачу.

Представление чисел с плавающей точкой имеет погрешность. Число пи имеет погрешность. 2*pi имеет еще большую погрешность, 3*pi еще больше, и так далее. Каким должно быть N в формуле N*pi, чтобы cos(N*pi) посчитанный на ieee-754 дал 0 или 1? (N в этой задаче — целое, разумеется)
Есть сайт с подробным объяснением работы чисел с плавающей запятой (как на русском, так и на английском). Хорошие пример, объясняются тонкости и неожиданные для математика свойства. Вот здесь. Сайт сырой, а вот именно этот материал в основе изложен полностью.
Что-то меня эта тема с «окнами» ввела в еще большее заблуждение. По логике автора оригинальной статьи получается, что чем большее число мы хотим представить, тем ниже будет точность. Это если я правильно понял «викторину».
Все так. Взял бумажку, освежил знания по привычным формулам и понял, что точность теряется.
В хроме нажал F12, в консоле вписал:
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
Only those users with full accounts are able to leave comments. Log in, please.