Форум программистов, компьютерный форум, киберфорум
Наши страницы
Evg
Войти
Регистрация
Восстановить пароль
Рейтинг: 4.43. Голосов: 7.

Представление вещественных чисел "на пальцах"

Запись от Evg размещена 15.02.2012 в 13:29
Обновил(-а) Evg 04.12.2018 в 22:11

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме

Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи.

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

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





1. Общие сведения

По поводу машинного представления в машине плавающих чисел имеется целая куча статей, а так же об этом написано почти в любом учебнике. Но практика показывает, что начинающим этот момент понять сложно, даже при наличии описания. Я собираюсь пояснить только принцип представления плавающих чисел "на пальцах", после чего, как мне кажется, понять конкретное представление будет уже проще. А может и вообще этого делать не понадобится (для общего понимания такого описания может оказаться достаточным).

Пояснить попробую на примере десятичного представления чисел, т.к. это проще для восприятия. Представим, что у нас есть табло, состоящее из шести индикаторов, на каждом из которых может быть представлена цифра от нуля до девяти. Значение "шесть" взято от балды. Конкретное количество индикаторов не важно, важно лишь то, что их ограниченное количество (поскольку в машине любой тип данных имеет вполне конкретный ограниченный размер). Допустим, что между первым и вторым индикатором нарисована десятичная запятая (или точка, кому как удобнее), а между четвёртым и пятым - некий разграничитель (нпример, вертикальная чёрточка). Таким образом шесть нулей будут отображаться в виде "0.000|00".

Теперь возьмём некотрое вещественное число, например, 153.7. Запишем число в экспотенциальной форме: 1.537*10^2. При этом часть "1.537" по научному называется мантисса, а "2" - экспонента. Теперь отобразим такую форму записи значения на табло и получим такое состояние: "1.537|02" (при этом "10" мы не пишем, поскольку мы "знаем", что работаем в десятичной системе). Напомню, что десятичная точка и разграничитель "нарисованы" на табло (т.е. они всегда находятся в одних и тех же позициях и не могут быть сдвинуты или перерисованы). В итоге мы наше вещественное число неким хитрым образом отобразили на наше табло, которое обладает тем свойством, что десятичная точка всегда находится между первым и вторым индикатором, а разграничитель - между четвёртым и пятым. Теперь можно стереть десятичную точку и разграничитель, в результате мы получим как бы целое число "153702", в котором на самом деле закодировано вещественное значение 153.7. В машине, которая умеет работать только с двоичными числами, вещественные значения хранятся (кодируются) примерно по такому же принципу: имеется N-разрядная ячейка памяти, часть разрядов которой отведена под мантиссу (записанную в двоичной форме), а часть - под экспоненту (тоже в двоичной форме), при этом машина "знает", что она работает в двоичной системе (аналогично тому, как мы "знали", что работаем в десятичной системе).

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

2. Приблизительные значения

Допустим, у нас имеется число 234.567. После переведения его в экспотенциальную форму мы получаем 2.34567*10^2, а при дальнейшем записи на табло получаем "2.345|02". Мы видим, что мантисса не может целиком влезть в те 4 индикатора, которые под неё отведены. Из этого следует, что в такой форме можно хранить не точные, а лишь приблизительные значения. Но не всегда, потому как для числа 12.34 мы можем сохранить точное значение, потому как мантисса экспотенциального представления числа аккуратно влезет в наши 4 разряда: "1.234|01"

Если мы посмотрим на пример

C
#include <stdio.h>
 
float f = 123456.123456;
 
int main (void)
{
  printf ("%f\n", f);
  return 0;
}
то увидим на исполнении, что наше число представлено в машине не точно, а приблизительно (округлённо)

Код:
$ gcc t.c
$ ./a.out
123456.125000
3. Разная точность для больших и маленьких чисел

Рассмотрим числа из разных диапазонов:
  • Число 1.23456, отображённое на табло, будет выглядеть как "1.234|00", что есть 1.234 при обратном раскодировании. Т.е. представлено с точностью до 0.001.
  • Число 123.456, отображённое на табло, будет выглядеть как "1.234|02", что есть 123.4 при обратном раскодировании. Т.е. представлено с точностью до 0.1.
  • Число 123456, отображённое на табло, будет выглядеть как "1.234|05", что есть 123400 при обратном раскодировании. Т.е. представлено с точностью до 100 (таким образом, мы даже целую часть потеряли).

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

C
#include <stdio.h>
 
float f1 = 1.2345678912345;
float f2 = 12345678912.345;
 
int main (void)
{
  printf ("%.10f\n", f1);
  printf ("%.10f\n", f2);
  return 0;
}
Код:
$ gcc t.c
$ ./a.out
1.2345678806
12345678848.0000000000
Из печати мы видим, что напечатанные числа даже немного не похожи на наши оригинальные. Всё это из-за того, что записываем мы их в десятичной форме, но в машине они хранятся в двоичной форме (и округляются не с точностью до десятичных целых и дробных, а с точностью до двоичных)

4. Потери точности при вычислениях

Математически (1 / 3) * 3 строго равняется единице. Но если мы всё это воспроизведём через наше табло, то получим, что 1 / 3 = "0.333|00", а (1/3) * 3 = "0.333|00" * 3 = "0.999|00", что вовсе не равно единице. Теперь расмотрим другое выражение: (1 / 30) * 30 (которое математически так же равно единице). 1 / 30 = "0.033|00", "0.033|00" * 30 = "0.990|00". Т.е. мы получили, что значение выражения "(1 / 3) * 3" с точки зрения нашего представления НЕ эквивалентно "(1 / 30) * 30".

Теперь рассмотрим сложение большого и маленького значения: 1234 + 0.5. Сумма должна быть 1234.5, но эта сумма, записанная на нашем табло, будет отражаться как "1.234|03", т.е. эквивалентна первому слагаемому и мы имеем эффекто того, что "a + b == a"

Едем дальше. Рассмотрим два казалось бы эквивалентных выражения "1234 + 0.5 - 1234" и "1234 - 1234 + 0.5". Эти выражения отличаются только порядком слагаемых. Для первого выражения будем иметь 1234 + 0.5 == 1234 (как следует из предыдущего абзаца), и итоговое значение выражения будет равняться нулю. Для второго выражения 1234 - 1234 равняется нулю и итоговое значение выражения равно 0.5. Т.е. правило ассоциативности "(a + b) + c == a + (b + c)" для вещественных чисел в машинном представлении в общем случае неверно

Такие "странные" эффекты являются следствием двух свойств, описанных в предудущих разделах: мы храним приблизительные значения и точность представления маленьких и больших значений разная.

5. Представление чисел, меньших единицы

FIXME дописать

6. Представление отрицательных чисел

FIXME дописать

7. Сравнение плавающих чисел при помощи целочисленного сравнения

FIXME дописать

За основу взят пост http://www.cyberforum.ru/post766330.html

8. Краткие итоги

Многие могли наблюдать такие эффекты, что при печати чисел с большим количеством знаков после запятой в конце появляются "лишние" разряды. Например, число 0.8 является "точным" (т.е. записывается в виде конечного количества значащих разрядов после запятой), однако при печати можно увидеть "хвост". Это является следствием того, что в машинах числа хранятся в двоичном представлении, и при этом не всякое "точное" число в десятичном представлении будет являться "точным" в двоичном представлении: числа 1.25 или 0.75 при переводе в двоичное представление будут "точными", а 0.8 или 1.7 - нет. С другой стороны число 1/3, записанное в десятичном представлении НЕ будет точным, а в троичном - будет. Именно те причины, которые описаны в статье, являются проблемным местом всех вещественных вычислений на процессорах, просто эти эффекты происходят на двоичном представлении, а не на десятичном: природа всех эффектов в двоичном представлении точно такая же, но в деталях и конкретных значениях выглядит немного не так, как в десятичном представлении. Но всю конкретику можно понять только усвоив, каким образом формируется запись дробных чисел в различных системах счисления.

На мой взгляд сделанные "на пальцах" пояснения на примере десятичных чисел помогут начинающим понять подобные "странные" эффекты, которые наблюдаются при работе с вещественной арифметикой, и дадут общее представление о хранении вещественных чисел в машине. Для тех, кому стала интересна конкретика, мне остаётся лишь подкинуть ссылки на статьи по конкретному представлению вещественных чисел в машине, где всё уже расписывается с точностью до бита и согласно стандарту (см. ссылки в разделе 11). Цель моего пояснения сводилась лишь к тому, чтобы дать некие базовые понятия, которые облегчат процесс восприятия при чтении подобных статей и книг.

9. Одно из стандартных заблуждений

FIXME явным образом написать об этом, а пока в виде ссылки: Неправильное значение переменной в for`e

10. Ссылки на темы, где обсуждался данный вопрос

11. Внешние ссылки по данной тематике

12. Программа для наглядности

snake32 предложил программу-визуализатор, которая по простому позволяет "пощупать" 32-битные плавающие числа в двоичном представлении. Программа немного вываливается за рамки статьи, поскольку в статье были объяснены только базовые принципы и даже не было описания хранения вещественных чисел в двоичном формате. Но если есть полезная в обучении вещь, то почему бы её не прицепить. Скачать программу: SinglePrecision.rar. Скачать исходники на Delphi XE 2: SinglePrecision.zip



Нажмите на изображение для увеличения
Название: singleprecision.jpg
Просмотров: 2406
Размер:	37.6 Кб
ID:	1915
Всего комментариев 3
Комментарии
  1. Старый комментарий
    Запись от LVV размещена 02.03.2017 в 07:18 LVV вне форума
  2. Старый комментарий
    Статья супер!! Автору огроменное спасибо и уважение. Действительно "на пальцах" все просто и понятно.
    Запись от txtbit размещена 26.07.2018 в 19:54 txtbit вне форума
  3. Старый комментарий
    Аватар для D1973
    Вот прямо респектище!!!
    У сына вопрос по информатике возник как раз по этому поводу. Сам умом-то это понимаю, а объяснить внятно не смог...
    Теперь и у того в мозгах прояснение появилось, спасибо!
    Запись от D1973 размещена 11.01.2019 в 07:50 D1973 вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2019, vBulletin Solutions, Inc.
Рейтинг@Mail.ru