picolisp.ru
Переключить темный/светлый/авто режим Переключить темный/светлый/авто режим Переключить темный/светлый/авто режим Back to homepage

Арифметика с плавающей точкой против арифметики с фиксированной точкой

Перевод. Оригинал здесь

Это десятая статья из серии Введение в 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, который до сих пор поддерживается и используется в низкоуровневых, "железных", приложениях благодаря своей гибкости, скорости и компактности исходного кода.