Арифметика с плавающей точкой против арифметики с фиксированной точкой
Перевод. Оригинал здесь
Это десятая статья из серии Введение в PicoLisp.
Теперь, когда вам знакомы основные функции языка PicoLisp, давайте попытаемся понять особенности вычислений с фиксированной точкой. Материал основан на этой оригинальной статье.
Если вы внимательно читали предыдущие статьи, вы наверняка задавались вопросом: "Почему в PicoLisp отсутствуют вычисления с плавающей точкой?"
Что это означает в практическом смысле? Это означает, что все вычисления ведутся над целыми числами. Посмотрите на этот код:
:(sqrt 2)
-> 1
:(+ 1.7 2.4)
-> 4
Получается, мы не можем совершать точные вычисления? Конечно, можем. Идея принятого в PicoLisp подхода заключается в том, что точность вычислений должна задаваться программистом. Например, если мы устанавливаем точность 3, то все десятичные числа будут автоматически умножаться виртуальной машиной на 10^3, то есть на 1000. Это означает, что мы достигаем большей точности, но числа становятся длиннее.
В реальном мире нет ничего необычного в таком подходе. Представьте на минуту, что вы швейцарский часовой мастер. Вы работаетеис очень маленькими деталями, и масштабы предметов, с которыми вы работаете, измеряются в миллиметрах. Это равнозначно умножению всех размеров на 10^3. Ещё пример. Счёт за воду насчитывается в кубических метрах. Однако молоко вы покупаете в литрах, а в медицине большинство измерений жидкости происходит в миллилитрах. Питательность пищи мы измеряем в калориях, счёт за электричество в киловатт-часах, мощность мотора в лошадиных силах. Для перехода от одной единицы измерения к другой мы умножаем/делим число на определённый коэффициент, и очень часто таким коэффициентом являются степени числа 10.
Давайте попробуем установить фактор точности в 3 (=10^3) и посмотрим,
что у нас получится. Фактор точности хранится в глобальной переменной
*Scl
, а также может быть получен с помощью функции scl
.
: (scl 3)
-> 3
: (setq A 3)
-> 3
: (setq B 3.0)
-> 3000
: (+ A B)
-> 3003
: (+ B B)
-> 6000
Что происходит в этом коде? Когда мы присваиваем символу A значение 3, то интерпретатор воспринимает 3 как обычное целое число. Однако, когда мы присваиваем символу B значение 3.0, интерпретатор обрабатывает его как число с фиксированной точкой, и умножает его на 1000, в соответствии с установленным фактором точности. Поэтому, когда мы складываем A и B, то получаем не 6, не 6000, а 3003. Какой вывод можно из этого сделать? Что при вычислениях с фиксированной точкой все числа должны содержать в себе точку.
Ещё одна особенность: числа автоматически округляются:
: 0.33453
-> 335
В общем, со сложением и вычитанием дело обстоит просто. Мы просто держим в уме фактор точности, и этого достаточно.
Другое дело с умножением и делением. При выполнении этих операций факторы точности складываются, и для получения правильного результата нужно немного хитрости.
Давайте попробуем произвести вот такое умножение: 3.0*3.0
.
: (* 3.0 3.0)
-> 9000000
Мы получили 9 миллионов!!! Так происходит оттого, что факторы точности
тоже перемножаются, и в реальности происходит вот такое вычисление:
(3*3)*(10^3*10^3) = 9*10^6
.
Для умножения и деления чисел с фиксированной точкой в PicoLisp есть
специальная функция умножения-деления: */
. Она пнремножает все
числа, кроме последнего, а затем делит произведение на последнее число.
В нашем случае это будет выглядеть так:
:(*/ 3.0 3.0 1.0)
-> 9000
Частное чисел A и B может быть посчитано как (*/ 1.0 A B)
.
: (*/ 1.0 2.0 5.0)
-> 400
Выглядит лучше, верно? Наверняка такой метод вычислений вам непривычен и вызывает смущение. На то, чтобы к нему привыкнуть, уйдёт некоторое время.
Теперь мы знаем, что нужно сделать для исправления нашего примера с
квадратным корнем. Вычисление квадрарного корня - своеобразный вариант
деления, поэтому функция sqrt
принимает второй параметр, чтобы
разделить результат:
: (scl 3)
-> 3
: (sqrt 2.0 1.0)
-> 1414
Иногда нам нужно представление чисел без фактора точности, например,
чтобы перепроверить значение, или вывести его на печать. Для этого в
PicoLisp есть две встроенные функции: format
и round
. format
возвращает число в виде строкового представления, используя несколько
опций форматирования (подробнее оь этом читайте в
документации), round
округляет число до требуемой точности.
: (scl 3)
-> 3
: (format (*/ 2.5 3.5 1.0) *Scl)
-> "8.750"
: (round (*/ 2.5 3.5 1.0))
-> "8.750"
: (round (*/ 2.5 3.5 1.0) 1)
-> "8.8"
Поздравляем, вы теперь в курсе основных особенностей вычислений с фиксированной точкой!
У вас до сих пор может остаться вопрос "А что же не так с числами с плавающей точкой???". Отвечаем:
- Простота - это здорово.
- Вычисления с плавающей точкой по умолчанию неточны, в отличие от целочисленных и с фиксированной точкой.
Числа с плавающей точкой округляют значения до точности 56 бит (в случае 64-битных float-ов). Это может привести к накапливающейся ошибке вычислений. Вот пример из программы на Си:
int main(void) {
double d = 0.1;
int i;
for (i = 0; i < 100000000; ++i)
d += 0.1;
printf("%9lf\n", d);
}
Этот код выводит результат:
10000000.081129
Соответствующий код на PicoLisp:
(scl 6)
(let D 0.1
(do 100000000
(inc 'D 0.1) )
(prinl (format D *Scl)) )
(bye)
выводит правильный результат:
10000000.100000
Арифметика с фиксированной точкой взята не из языка Lisp. Функция */
,
так же, как и некоторые другие минималистичные концепции PicoLisp, взята
из языка
Forth,
который до сих пор поддерживается и используется в низкоуровневых,
"железных", приложениях благодаря своей гибкости, скорости и
компактности исходного кода.