Math error in simple FOR loop with decimal 0.01

Get technical support from the community & developers with specific X16 programs if you can't find the solution elsewhere
(for general non-support related chat about programs please comment directly under the program in the software library)
EbonHawk
Posts: 17
Joined: Thu Mar 16, 2023 10:37 am

Math error in simple FOR loop with decimal 0.01

Post by EbonHawk »

I have R42 of the emulator for Windows 10, and just doing a simple FOR NEXT loop with a step of 0.01 creates some rounding errors fairly quickly.

100 FOR T = 1 TO 1.4 STEP 0.01
200 PRINT T
300 NEXT T

Generates this output:
(well, I thought I could copy & paste it, but can't figure out how at the moment)

It starts out ok with outputting..
1
1.01
1.02 ... etc

But, after the decimal gets to 1.23 instead of 1.23 it starts printing
1.2299999..
1.2399999..
1.2499999..

And I even tried just simply the same loop with another variable incremented by +0.01 each time and the same result happens.

Is this universal? Can anyone else confirm this happens on their emulator as well?

And can this be fixed? Something this simple shouldn't be in the math processes already!!
grml
Posts: 60
Joined: Sat Aug 13, 2022 8:31 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by grml »

What you're seeing is floating point numbers and their limited precision. BASIC uses floating point numbers because they're easy to use even though they are imprecise, and slow too, because the hardware does not support them.

To store numbers with infinite precision, infinite memory would be required. The X16 does not have infinite memory, so it can't store numbers with infinite precision, and we must settle for something less precise instead. Most people are shocked when they first learn that their computer doesn't count right, but there's nothing to be worried about. Everything is working as intended.

How these numbers work: https://en.wikipedia.org/wiki/IEEE_754 (the C64's numbers were slightly different from this, I don't know about the X16)
kelli217
Posts: 531
Joined: Sun Jul 05, 2020 11:27 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by kelli217 »

If you want to copy & paste output from the emulator, you'll have to turn 'echo' on from the command line.

x16emu -echo
DragWx
Posts: 341
Joined: Tue Mar 07, 2023 9:07 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by DragWx »

This is a pretty normal thing that happens on PCs too, so it's not unique to the X16 nor its emulator. The first time it happened to me, it was in QBASIC two decades ago, and in my case, the workaround was to count by 1 instead of 0.01, and then divide the variable by 100 when I wanted to display it to the screen, so it'd always show up correctly.
EbonHawk
Posts: 17
Joined: Thu Mar 16, 2023 10:37 am

Re: Math error in simple FOR loop with decimal 0.01

Post by EbonHawk »

Interesting.. I don't recall ever seeing this before, but okay.

I would have thought incrementing a separate variable by 0.01 would have been an adequate workaround with the second set, but apparently not.

Just adding 0.01 to itself 23 times shouldn't introduce any errors, but if that's what it is, then that's that.

Thanks for the clarification. :)
User avatar
StephenHorn
Posts: 565
Joined: Tue Apr 28, 2020 12:00 am
Contact:

Re: Math error in simple FOR loop with decimal 0.01

Post by StephenHorn »

The important thing to understand about why this happens with a value like "0.01" instead of "1" is because of how "0.01" is represented in binary, as opposed to "1".

Let's set aside the complexity of IEEE floats and just consider fixed-precision point, which is just the binary version of saying "the same number of digits after the decimal". We'll keep things simple with 8 digits after the decimal.

1.00000000 in decimal would be 00000001 00000000 in binary.
2.00000000 in decimal would be 00000010 00000000 in binary.
But 0.10000000 in decimal would be 00000000 00011001 in binary... almost. It's actually 0.9765625. We have to approximate, because instead of each bit representing 0.1, 0.01, 0.001, etc., each bit is 0.5, 0.25, 0.125, 0.0625, etc., and these powers of two will never add up to the decimal value of "0.1".

And that means error creeps in when we start adding these together:
00000000.00011001 + 00000000.00011001 = 00000000.00110010, or 0.1953125.
00000000.00110010 + 00000000.00011001 = 00000000.01001011, or 0.29296875.
00000000.01001011 + 00000000.00011001 = 00000000.01100100, or 0.390625.

Okay, now let's come back to IEEE floats.

16-bit IEEE floats have the same problem, but do so in the binary equivalent of scientific notation. Instead of 1e-1, it's 0 01011 1001100110: a sign bit (0 for positive values), an exponent of "-4" in binary, and the binary value 1.1001100110 except we can omit the bit in front of the decimal since, being scientific notation and binary, we know it will always be "1"). This allows us to get closer to 0.1, but it's still an approximation (0.0999755859375), and adding them together still introduces error that leads to these weird numbers you're seeing.

If you try this on a modern x86 and don't see the same result, the main reason is because most CPUs implement floating point in 32-bits instead of 16-bits, which allow much better approximations still, and actually most floating-point registers aren't actually 32-bits, either; they're 35 or 36 bits (or even more) so they can perform common floating point arithmetic and then have leftover precision for accurate rounding. And libraries to print these floats can have their own rounding rules to try and accurately reflect the value they think the floating point number represents, even when it can't cleanly round to a decimal figure.
Last edited by StephenHorn on Thu Mar 16, 2023 7:58 pm, edited 1 time in total.
Developer for Box16, the other X16 emulator. (Box16 on GitHub)
I also accept pull requests for x16emu, the official X16 emulator. (x16-emulator on GitHub)
cuteswan
Posts: 5
Joined: Thu Oct 20, 2022 11:53 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by cuteswan »

If you want to dig a little deeper, here is a decent video on floating point encoding that popped up in my feed a few days ago: https://www.youtube.com/watch?v=Oo89kOv9pVk

And this online tool lets you play directly with the bits of a 32-bit float and see the conversion errors in real time: https://www.h-schmidt.net/FloatConverter/IEEE754.html

BTW I could have sworn that some implementations of BASIC let you use a # suffix to make a double floating point, but no luck on Apple ][, Maximite, or even the online DEC PDP/11 emulator. Maybe I'm thinking of something else.
BruceRMcF
Posts: 224
Joined: Sat Jan 07, 2023 10:33 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by BruceRMcF »

cuteswan wrote: Thu Mar 16, 2023 6:36 pm... BTW I could have sworn that some implementations of BASIC let you use a # suffix to make a double floating point, but no luck on Apple ][, Maximite, or even the online DEC PDP/11 emulator. Maybe I'm thinking of something else.
QBasic uses ! for single floats and # for double floats. CBM V2.0 floats have more precision in the mantissa than QBasic Singles, since they are 40bit floats rather than 32bit floats.
cuteswan
Posts: 5
Joined: Thu Oct 20, 2022 11:53 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by cuteswan »

BruceRMcF wrote: Thu Mar 16, 2023 6:47 pm QBasic uses ! for single floats and # for double floats. CBM V2.0 floats have more precision in the mantissa than QBasic Singles, since they are 40bit floats rather than 32bit floats.
Ah good, so I'm not losing my mind after all. (Well, at least not in this instance ;) )
TomXP411
Posts: 1781
Joined: Tue May 19, 2020 8:49 pm

Re: Math error in simple FOR loop with decimal 0.01

Post by TomXP411 »

This is a clear reason why you should avoid fractional steps on FOR loops. It's always a good idea to use whole numbers as control variables and step values, if you care about landing on a specific value or going a specific number of steps.

The issue is that every number base suffers from the curse of irrational numbers. An irrational number is a fractional value that does not resolve to an exact figure, like 1/7 or Pi.

So while 1/100 in base-10 is 0.01 decimal form, it's an irrational number in binary form.

The solution is to use whole numbers: whole numbers in BASIC have perfect precision up to +/- 32 bits.

So going back to your original example: FOR T = 1 TO 1.4 STEP 0.01

Again, always use whole numbers. One way to do so is simply move the decimal point to the right two times. That means your values are: 100 to 140. Then move the decimal point back to the left to actually print your result.

Here's an example:

100 FOR T = 100 TO 140
200 PRINT T / 100
300 NEXT T
Post Reply