DEV Community

Cover image for Floating Point vs. Fixed Point Arithmetics
Mia
Mia

Posted on • Originally published at picolisp-blog.hashnode.dev

Floating Point vs. Fixed Point Arithmetics

Welcome back to our "PicoLisp for beginner's series"!

Now that we know the basic functions, let's try to understand now how calculations are done internally in PicoLisp. This text got inspiration from this blogpost.


If you read the previous posts carefully, you might have already wondered because something seems missing: Floating point numbers!

In reverse, this means: All calculations are integer calculations. Let's try it out:

:(sqrt 2)
-> 1

:(+ 1.7 2.4)
-> 4
Enter fullscreen mode Exit fullscreen mode

But what does that mean? We cannot do precise calculations? Of course we can. The idea behind is that the scale needs to be defined by the programmer first. For example, if we set a scale of 3, every number with a decimal point will be internally multiplied by 10^3. This means, we gain higher precision but the numbers obviously get longer.

In the real world, this is nothing special: For example let's assume you are a Swiss Clockmaker. It means you would be dealing with tiny tiny parts and the scale of our blueprint will certainly be in millimeter. This is just the same like multiplying all values by 10^3. Another example: The yearly water bill is calculated in m³, but we buy our milk in liters, and medicine in milliliters. We count calories, pay electricity bills in kWh and drive cars with horsepower. In order to get from one dimension to the other, we usually need to multiply by some kind of scaling factor (very often magnitudes of 10).


Addition and Subtraction

Okay, so let's set the scale to 3 (=10^3) and see what happens. (The scale is set in the global variable *Scl and can also be accessed by calling scl.)

: (scl 3)
-> 3
: (setq A 3)
-> 3
: (setq B 3.0)
-> 3000
: (+ A B)
-> 3003
: (+ B B)
-> 6000
Enter fullscreen mode Exit fullscreen mode

What is happening here? First we set variable A to 3. Since there is no dot in the number, the reader interprets it as "normal" integer, i. e. "3". Then we set variable B to 3.0. The reader interprets this as 3.0*10^3, i.e. "3000". So if we add A and B, we get 3003, and the last three digits are decimal places. So we make a mental note: All numbers within a calculation should contain the decimal point.

One more note: values are automatically rounded.

: 0.33453
-> 335
Enter fullscreen mode Exit fullscreen mode

So, for addition and subtraction it's fairly easy: We just keep in mind that we are calculating in another scale and that's it. No need to do more.

Multiplication and Division

However when we talk about multiplication and division, we need to be more careful because the scaling factor is inherited (think about converting cubic meters to liters or hectare to mm² - it's a little bit tricky).

If we just naively try to calculate 3.0*3.0, this is what we get:

: (* 3.0 3.0)
-> 9000000
Enter fullscreen mode Exit fullscreen mode

9 Million! This is wrong for sure. This is because the scaling factor has also been multiplied, so actually what we have done is this: (3*3)*(10^3*10^3) = 9*10^6.

For this purpose, PicoLisp has a multiply-divide function called */ built in. It multiplies all numbers except for the last one, and then divides the result by the last one. In our example:

:(*/ 3.0 3.0 1.0)
-> 9000
Enter fullscreen mode Exit fullscreen mode

And the quotient of A and B can be computed as (*/ 1.0 A B).

: (*/ 1.0 2.0 5.0) 
-> 400
Enter fullscreen mode Exit fullscreen mode

Looks better, right? (...If you cannot honestly answer with "YES", go back to the beginning and think through it again! It's not what we're used to, and it takes some time to overcome that.)


In order to fix our square root example from above, we now understand what to do. Square root calculations can be interpreted are some kind of division, and for this purpose the sqrt function takes another parameter to divide the result:

: (scl 3)
-> 3
: (sqrt 2.0 1.0)
-> 1414
Enter fullscreen mode Exit fullscreen mode

Formatting

Sometimes we might still want to read the number without scaling factor, for example to double-check the value, or for some printing output. For this, PicoLisp has two built-in functions: format or round. format returns the value as a string or vice versa (with some formatting options, see docs), round rounds it to the number of digits specified according to the given scale.

: (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"
Enter fullscreen mode Exit fullscreen mode

Congratulations, now you have mastered the key points of fixed point arithmetics!


...still, you might ask yourself:

...OKAY, BUT WHY THE TROUBLE?!

Basically, there are two main reasons:

  • Reason #1. Simplicity is Beauty
  • Reason #2. Scaled integer arithmetics are exact, floating point numbers are not.

Floats cannot represent most numbers exactly, but round them to 56 bits (in case of double precision numbers). This may cause errors to be accumulated. For example, this C program

int main(void) {
   double d = 0.1;
   int i;
   for (i = 0; i < 100000000; ++i)
      d += 0.1;
   printf("%9lf\n", d);
}
Enter fullscreen mode Exit fullscreen mode

prints

10000000.081129
Enter fullscreen mode Exit fullscreen mode

while a corresponding picolisp program

(scl 6)
(let D 0.1
   (do 100000000
      (inc 'D 0.1) )
   (prinl (format D *Scl)) )
(bye)
Enter fullscreen mode Exit fullscreen mode

prints

10000000.100000
Enter fullscreen mode Exit fullscreen mode

which is correct.


Fun fact for linguists

Fixed point arithmetics are not a Lisp feature. In fact, the */ function as well as some other minimalistic PicoLisp concepts are heavily inspired by the programming language [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language) that is still maintained today and used for hardware applications that require flexibility, speed and compact source code.


Sources

Top comments (0)