В начале 90-х создание трёхмерного игрового движка означало, что вы заставите машину выполнять почти не свойственные ей задачи. Персональные компьютеры того времени предназначались для запуска текстовых процессоров и электронных таблиц, а не для 3D-вычислений со частотой 70 кадров в секунду. Серьёзным препятствием стало то, что, несмотря на свою мощь, ЦП не имел аппаратного устройства для вычислений с плавающей запятой. У программистов было только АЛУ, перемалывающее целые числа.
При написании книги Game Engine Black Book: Wolfenstein 3D я хотел наглядно показать, насколько был велики были проблемы при работе без плавающей запятой. Мои попытки разобраться в числах с плавающей запятой при помощи каноничных статей мозг воспринимал в штыки. Я начал искать другой способ. Что-нибудь, далёкое от и их загадочных экспонент с мантиссами. Может быть, в виде рисунка, потому что их мой мозг воспринимает проще.
В результате я написал эту статью и решил добавить её в книгу. Не буду утверждать, что это моё изобретение, но пока мне не приходилось видеть такого объяснения чисел с плавающей запятой. Надеюсь, статья поможет тем, у кого, как и меня, аллергия на математические обозначения.
Как обычно объясняют числа с плавающей запятой
Цитирую Дэвида Голдберта (David Goldbert):
Для многих людей арифметика с плавающей запятой кажется каким-то тайным знанием.
Полностью с ним согласен. Однако важно понимать принципы её работы, чтобы полностью осознать её полезность при программировании 3D-движка. В языке C значения с плавающей запятой — это 32-битные контейнеры, соответствующие стандарту IEEE 754. Они предназначены для хранения и выполнения операций над аппроксимациями вещественных чисел. Пока я видел только такое их объяснение. 32 бита разделены на три части:
- S (1 бит) для хранения знака
- E (8 бит) для экспоненты
- M (23 бита) для мантиссы
Внутренности числа с плавающей запятой.
Три части числа с плавающей запятой.
Пока всё нормально. Пойдём дальше. Способ интерпретации чисел обычно объясняется с помощью такой формулы:
Именно это объяснение чисел с плавающей запятой все ненавидят.
И здесь я обычно начинаю терять терпение. Возможно, у меня аллергия на математическую нотацию, но когда я это читаю, в моём мозгу ничего не «щёлкает». Такое объяснение похоже на способ рисования совы:
Другой способ объяснения
Хоть это изложение и верно, такой способ объяснения чисел с плавающей запятой обычно не даёт нам никакого понимания. Я виню эту ужасную запись в том, что она разочаровала тысячи программистов, испугала их до такой степени, что они больше никогда не пытались понять, как же на самом деле работают вычисления с плавающей запятой. К счастью, их можно объяснить иначе. Воспринимайте экспоненту как окно (Window) или интервал между двумя последовательными степенями двух целых чисел. Мантиссу воспринимайте как смещение (Offset) в этом окне.
Три части числа с плавающей запятой.
Окно сообщает нам, между какими двумя последовательными степенями двойки будет число: [0,1], [1,2], [2,4], [4,8] и так далее (вплоть до [,]. Смещение разделяет окно на сегментов. С помощью окна и смещения можно аппроксимировать число. Окно — это отличный механизм защиты от выхода за границы. Достигнув максимума в окне (например, в [2,4]), можно «переплыть» вправо и представить число в пределах следующего окна (например, [4,8]). Ценой этого будет только небольшое снижение точности, потому что окно становится в два раза больше.
Викторина: сколько точности теряется, когда окно закрывает больший интервал? Давайте возьмём пример с окном [0,1], в котором 8388608 смещений накладываются на интервал размером 1, что даёт нам точность . В окне [2048,4096] 8388608 смещений накладываются на интервал , что даёт нам точность .
На рисунке ниже показано, как кодируется число 6,1. Окно должно начинаться с 4 и заканчиваться следующей степенью двойки, т.е. 8. Смещение находится примерно посередине окна.
Значение 6,1 аппроксимированное с помощью числа с плавающей запятой.
Давайте возьмём ещё один пример с подробным вычислением представлением в виде числа с плавающей точкой хорошо известного всем нам значения: 3,14.
- Число 3,14 положительно .
- Число 3,14 находится между степенями двойки 2 и 4, то есть окно числа с плавающей запятой должно начинаться с (см. формулу, где окно — это ).
- Наконец, есть смещений, которыми можно выразить расположение 3,14 внутри интервала [2-4]. Оно находится в внутри интервала, что даёт нам смещение
В двоичном виде это преобразуется в следующее:
- S = 0 = 0b
- E = 128 = 10000000b
- M = 4781507 = 10010001111010111000011b
Двоичное представление с плавающей точной числа 3,14.
То есть значение 3,14 аппроксимируется как 3,1400001049041748046875.
Соответствующее значение в непонятной формуле:
И, наконец, графическое представление с окном и смещением:
Окно и смещение числа 3,14.
Интересный факт: если модули операций с плавающей запятой были такими медленными, почему в языке C в результате использовали типы float и double? Ведь в машине, на которой изобретался язык (PDP-11), не было модуля операций с плавающей запятой! Дело в том, что производитель (DEC) пообещал Деннису Ритчи и Кену Томпсону, что в следующей модели он будет. Они были любителями астрономии и решили добавить в язык эти два типа.
Интересный факт: те, кому в 1991 году действительно нужен был аппаратный модуль операций с плавающей запятой, могли его купить. Единственными, кому он мог понадобиться в то время, были учёные (по крайней мере, так Intel понимала потребности рынка). На рынке они позиционировались как «математические сопроцессоры». Их производительность была средней, а цена огромной (200 долларов 1993 года — это 350 долларов в 2016 году.). В результате уровень продаж оказался посредственным.
Надеюсь, статья была вам полезна!
rkfg
Более-менее понятно. Интересное следствие: из такой формы следует, что уже у сравнительно небольших чисел, порядка 2^24, падает точность представления даже целой части (потому что мантисса делит всегда на 2^23 элемента, и если элементов между двумя точками будет больше, то каждому второму не достанется своего значения):
Выведет 0 и 1, т.е. во float-представлении 16777216 равняется 16777217. Мне кажется, этот пример даже интереснее известного многим 0.1+0.2.
Интересно было бы почитать в доступной форме, как производятся математические операции с float-числами в таком виде и как можно эффективно перевести число из обычной строки во float.
ToSHiC
В свете вашего комментария стоит отметить, что в javascript все числа — это double. Там, конечно, диапазон целых значений, вычисляемых без погрешности, больше, но он заметно меньше, чем 2^64.
domix32
После некоторых трюков там можно оперировать 32-битными целыми числами
Ares_ekb
Есть ещё интересный пример:
equand
А вот это интересно, для работы бух софта это учитывается? Хотя там 1/3 не бывает в принципе.
Gumanoid
Там используют фиксированную точку (или просто int).
ustaspolansky
Разве во флоат не до 7 знаков включительно? Возможно это связано с большим кол. во знаков и некорректной обработкой именно на выводе.
Я не программист, не ругайте.
rkfg
Там не в знаках считается, а в битах. В статье сказано, что мантисса составляет 23 бита, так что максимальная достижимая точность на целых числах будет, когда между 2^E и 2^(E+1) (отбросим для простоты -127) будет 2^23 чисел, тогда каждому числу можно противопоставить одно значение мантиссы. Если же этих чисел больше, то можно найти такое число, которому уже не сопоставишь своё выделенное значение мантиссы, вот я для примера выбрал участок, где 2^24 чисел, т.е. в два раза больше. Поэтому у чётных чисел есть своё значение мантиссы, а нечётным уже не хватает, и происходит «округление».
sbnur
Вообще то исходно архитектура процессоров разрабатывалась для выполнения арифметических операцуий с плавающей запятой — в частности, на таких системах я учился в свое время.
Но изобретение велосипедов сопутствует развитию — поэтому не удивляюсь
rfq
операции с плавающей запятой присутствовали в компьютерах с самого начала. Их не было только в специализированных компьютерах и самых дешевых компьютерах общего назначения.
tyomitch
Они с самого начала были в «больших» компьютерах (предназначенных в первую очередь для учёных), но очень долго отсутствовали в ПК (предназначенных в первую очередь для секретарш).
32bit_me
Мне как раз кажется понятной формула, и кажутся непонятными все эти объяснения с картинками. Формула сразу даёт понимание того, как с этими числами работать, то есть как складывать, умножать и делить, пользуясь их бинарным представлением.
К тому же тема не раскрыта до конца, потому что, кроме описанной здесь нормализованной формы, есть также денормализованная форма и разные нечисла, типа NaN и Inf.
Wano987
Я, конечно, могу ошибаться, но каждому — своё.
Лично я односимвольные регистрочувствительные имена переменных/классов без обильных комментариев воспринимаю достаточно посредственно. Так что мне эта статья была небесполезна.
WebConn
С формулой понятно, как оперировать этими значениями.
С картинкой получилось как-то более явно увидеть грабли, спрятанные в арифметике с плавающей точкой.
rfq
Это вы увидели только первый слой граблей. А там внизу еще целая куча. И основная — поддержка точности. Точность теряется на всех этапах вычислений, так что конечный результат может не иметь ничего общего с действительностью, несмотря на то, что все используемые в расчетах формулы были правильными.
slavap
Формула ужасна, объяснение, а особенно картинка, отличные. Наверное, нужно быть математиком, чтобы сходу понять, как эти числа делить глядя только на формулу.
Alozar
Проблема такой математической записи, что она понятна только тем кто понимает. Простите за каламбур. Математическая запись точна, отбрасывает всё лишнее, но обывателю в большинстве случаев вообще не понятна.
Я например смог понять как с ней работать только после прочтения этой статьи, где было всё объяснено человеческим языком и показано на конкретном примере. Теперь смотря на формулу, всё становится на свои места и понятно, почему она такая и как с ней работать. Но это требует не математического, а «человеческого» объяснения.
Другой пример — производная функции
Определение точно? Точно. Понятно? Нет. В ответ на это определение надо выяснять, что теперь делать с этим произведением и зачем оно вообще надо. Если же сказать, что производная функции — это скорость изменения функции в конкретной точке, становится понятно, но при этом не точно. Аналогичная история происходит в этой статье. Формула нужна для точного понимания, а картинки для первичного понимания сути.
32bit_me
Я с вами согласен в том смысле, что формула не даёт понимания, когда смотришь на неё в первый раз. Но для объяснения принципа представления вещественных чисел в формате IEEE 754 в нормализованной форме достаточно разобрать один-два примера.
От статьи на хабря я обычно жду полноты, то есть, хорошо было бы написать про денормализованные числа, про нечисла и правила их обработки, про разные варианты, предусмотренные стандартом, включая децимальный формат, а также 128- и 256-битные числа, про 80-битный формат, который был принят в x86 изначально, про разные исторические и необычные форматы (типа модульного логарифмического). Вот это было бы интересно.
Varim
экспонента — расстояние между шагами
мантиса — номер шага
в статье не увидел, почему записывают
3,14 = 1,57 *2 ( 2 в степени 1 равно 2, то есть экспонента 128-127 = 1)
вместо
3,14 = 3,14*1 (2 в степени 0 равно 1, то есть экспонента 127-127 = 0)
Varim
в двоичном виде мантиса 1.M, то есть выигрывают 1н бит, а в десятичном может быть 1.M или 9.M
Varim
2в23 * 0,57 = 4781506.56
igormu
Нет окна [0, 1]. Есть только [1/2, 1), [1/4, 1/2) и так далее. 0 в нормализованном представлении отобразить нельзя, поэтому он искусственно принят как (E=0, M=0).
GlebSemenov
Не могу согласиться с утверждением, что у процессора PDP-11 не было модулей плавающей точки. Был FIS (Floating Instruction Set) и кое-где FPP (Floating Point Processor)
DrAndyHunter
Статья доходчиво объясняет сложную для понимая вещь. Спасибо за перевод автору!
Но остался для меня один непонятный момент:
Я не понял, как вычисляется вот эта часть 0,0000001049041748046875?
GarryC
(1)10010001111....011b = 13170115d /4/2/2.../2 = 3,1400001049041748046875.
Den3D
M = 2^23*0.57 = 8388608 *0.57 = 4781506,56
4781507 — 4781506,56 = 0.44
0.44*2/8388608 = 0,0000001049041748046875
GalayZloy
А есть процессоры или теории с другим представлением чисел с запятой? Вообще описанное представление оптимально или исрользуется по-традиции?
MacIn
del
Busla
неправда:
типов с плавающей запятой несколько
они платформозависимы
IEEE 754 в С99 носит рекомендательный характер, а С11 ссылается на более поздние стандарты
MacIn
Как же вы будете читать статьи типа такой:
sites.math.washington.edu/~morrow/336_12/papers/ben.pdf
Если простая матзапись числа с плавающей запятой непонятна?
PaulZi
Может нубский вопрос, а чем интересно такое представление отличается в плане скорости/точности?
(-1)^S * 0.S * 10 ^ (E-127)
tyomitch
Возможно, вы имели в виду: (-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) однозначно, потому что ведущая единица в числе только одна.
PaulZi
Да, ошибся. Ну хорошо, допустим:
(-1)^S * 1.M * 10 ^ (E-127)
Чем это хуже чем:
(-1)^S * 1.M * 2 ^ (E-127)
?
С основанием 10 всё понимается в разы легче, по крайней мере для нас, 10-чных людей.
PaulZi
Хотя понял что с основанием 1.M не получится представить 2…
firk
Что там можно представить — вопрос вторичный, т.к. это решаемо в любом случае. Старшую единицу сделали неявной просто потому, что оказалось можно сэкономить 1 бит, вряд ли кто-то специально к этому стремился заранее.
А двоичные порядки потому что процессор вообще обычно двоичный, и создавать какую-то специальную недвоичную логику ради малопонятных целей резона нет. А там где надо вывести в понятном виде человеку — двоичные порядки легко конвертируются в десятичные соответствующей программой.
tyomitch
?
Andronas
Число 3,14 находится между степенями двойки 2 и 4, то есть окно числа с плавающей запятой должно начинаться с 2^1 >E=128 (см. формулу, где окно — это 2(E?127)).
откуда тут взялось что 2^1 >E=128 ??? как объяснить не привлекая формулу?
Westimo
Не привлекая формулу, можно считать так:
Окно | Е
…
[1,2] | 127
[2,4] | 128
[4,8] | 129
…