22

I am mildly curious that though the 6502 provides BCD arithmetic which would be useful for implementing decimal floating point, Commodore BASIC uses, like all (?) Micro-Soft BASIC, binary floating point instead.

Are there any easy example that show precision errors in Commodore BASIC, that would not be present if it would be based on decimal FP?

A classic test of the difference is 0.1 + 0.2 = 0.3; this evaluates to false in pretty much every modern language (since almost all of them use the IEEE floating point that is built into modern hardware). There is even a website devoted to this oddity: 0.30000000000000004.com

I tried this on a C64 emulator (which uses the same BASIC as the PET) and to my astonishment, it correctly evaluated to true. So did some other obvious tests like 0.1 * 10 = 1 and 0.1 + 0.9 = 1 but they worked as well.

What test would give a wrong answer on Commodore BASIC? That is, I'm not asking for a way to get it to demonstrate rounding errors per se; that much is trivial. I'm asking for a way to get it to give a wrong answer, not because it lacks infinite precision, but specifically for(simple) cases where decimal arithmetic would give the right answer. Some Commodore BASIC (MS-BASIC) equivalent to the 0.1 + 0.2 = 0.3 test on IEEE 754.

Raffzahn
  • 222,541
  • 22
  • 631
  • 918
rwallace
  • 60,953
  • 17
  • 229
  • 552
  • 6
    It might be worth to remember that the PET at first was A machine, not a business or home. If at all it was meant as a hobby computer. The differentiation in business or home is something that only evolved later - and with it's missing colour and sound abilities it moved into a (more) business range. – Raffzahn Jan 27 '19 at 12:52
  • 1
    @Raffzahn True! I tried it on an Apple II emulator just now and it passes the test, though iirc the Apple II ended up using Microsoft BASIC just like Commodore so that's not surprising. I'll try it on some others if I can find emulators with working keyboards. – rwallace Jan 27 '19 at 13:00
  • Perhaps this BASIC implements the notion of floating-point equality as 'the difference is very small". That's been suggested in programming languages before, but is a bad idea, since it can result in A = B and B = C but A != C. – dave Jan 27 '19 at 13:32
  • Modern programming languages and standard libraries tend to have the property that with the default output formatting and precision, every different internal representation of a number produces a different external ("printed") representation, and that the conversion is consistent in both directions. It is easy to "break" that property by outputting fewer significant digits, to get output that looks "accurate" even when strictly speaking it is not exact. – alephzero Jan 27 '19 at 13:47
  • I am pretty sure (and my computer confirms that) your basic claim this evaluates to false in pretty much every modern language is wrong. Did you try that specific example on a modern machine? – tofro Jan 27 '19 at 13:50
  • @alephzero Yes, that's why I think the test needs to use an explicit comparison operator instead of just eyeballing numeric output. – rwallace Jan 27 '19 at 14:00
  • @tofro Yes, in Python 3. What did you try it in? – rwallace Jan 27 '19 at 14:00
  • 3
    This question is out of the ordinary, and I must admit I really like it. Not at least as it looks for easy repeatable tests over some lengthy explanation. A true Engineering aproach :)) Would you mind to rephrase it (mostly the first paragraph) a bit to focus on the core issue of PET/Commodore FP - as the title already expresses it quite good (maybe also adding "... precision" in the title) I can do as well if you like me to. That way it'll make a great stop for others searching for hints in the same direction. – Raffzahn Jan 27 '19 at 14:47
  • 1
    @Raffzahn Thanks! The rephrase is an interesting idea, sure, please go ahead. – rwallace Jan 27 '19 at 15:25
  • I tried C on a MacBook. Even single precision floats give the proper result up to 10 decimal digits. – tofro Jan 27 '19 at 15:53
  • Wasn't floating point faster than a decimal datatype back then just as it is today? In that case it's not really surprising that they chose floating point. Neither datatype can accurately represent rational numbers, and the few use cases which care about exact representation of decimal values and not about exact representation of other rational numbers just aren't common enough to justify the slowdown. – kasperd Jan 27 '19 at 17:24
  • 2
    @tofro But that wasn't the question. It's not about if the representation with 10 decimal digits is the same but if the actual numbers are the same. So don't print and visually compare the output but actually compare the numbers and print the outcome of that comparison. – BlackJack Jan 27 '19 at 17:56
  • @kasperd No, Decimal FP isn't inherently faster or slower than Binary. The advantage of binary FP is due a reduced need for gates and more dense storage. D-FP needs about ~10% more gates and ~50% more storage bits to hold the same amount of data (thanks to memory being binary and not decimal) - within specialized storage this can be reduced to less than 50%, but still more than binary. These differences made B-FP cheaper than D-FP, and our modern CPUs are grown from a heritage of penny-pinching cheapnicks :) – Raffzahn Jan 27 '19 at 18:04
  • @Raffzahn Not sure where you get the 50% storage overhead from. log(16)/log(10) is only 1.2041. And if you are a just a little bit creative about storage then log(1024)/log(1000) is only 1.0034. In the end the performance of each will depend on what features the hardware has to help you. And the question is about the choice of representation on a specific CPU, and there is likely an inherent difference in their performance on that CPU. – kasperd Jan 27 '19 at 19:18
  • @kasperd Only if you use binary decimal. And yes, by using 10 bits per 3 digits you do use a further binary reduction (I did mention it, didn't I?) - but add complexity in adder circuitry. Now, for example, normalization only can be done per three digits, not per digit. But more important, there is way more to FP than storing it. – Raffzahn Jan 27 '19 at 19:23
  • @Raffzahn There are architectures which have instructions to operate directly on numbers stored as two digits per byte, which is 20.4% overhead not 50%. But those instructions don't give you a decimal datatype just like integer instructions aren't floating point. In the end performance matters and it depends on the hardware. And I don't know of any architecture where decimal performs better than floating point. – kasperd Jan 27 '19 at 19:48
  • @kasperd I'm not really sure what your argument is. Ofc, there are decimal architectures, not just two per byte, but many more variations. I' have also a hard time to see where you try to twist it again by no saying no DFP is faster (after stating that BFP is faster). If you look back, my argument was there is no difference in speed in a decimal based circuitry to a binary one as the elements are exactly the same. Difference only exist (if at all) when one is implemented using circuitry made for the other. – Raffzahn Jan 27 '19 at 19:58
  • @Raffzahn You say there is no difference. I say it depends on the hardware. I do not have a 6502 to test on. And I made no specific claim about the performance of either on a 6502 other than saying that floating point is likely faster than decimal. What I did say is that there is a difference on a modern CPU. I just tested on an i3 CPU and found floating point calculations to be at least 100 times faster than decimal. An actual measured difference of a 100-fold slowdown does not match your claim of no difference. – kasperd Jan 27 '19 at 20:25
  • @kasperd Even without going into details of your test, you tested it on a CPU with a binary FPU emulating a decimal one is comparing apples with oranges. All proven is that a CPU with a binary FPU excels at doing binary FP. It gives no hint about how a decimal FPU would perform. – Raffzahn Jan 27 '19 at 22:03
  • " saying that floating point is likely faster than decimal" hmm, this makes me curious about the code you used., could it be that you imply FP as being binary and decimal as being something else? The question was about binary floating point vs. decimal floating point - these are two ways to store and calculate floating point, not FP and somethinge else. – Raffzahn Jan 27 '19 at 22:08

6 Answers6

19

This example reveals a rounding error under Commodore BASIC V2.0:

  A=0.3:B=0.6:IF A+B<>0.9 THEN PRINT A+B-0.9

Running this on a C64 yields a difference of 2.32830644e-10. Other pairs that fail are 0.4+0.5, 0.6+0.1 and 0.8+0.1. Please note that also the order in which the numbers are summed up affects the result. 0.6+0.1-0.7 yields a difference, while 0.1+0.6-0.7 results to 0.

Peter B.
  • 4,447
  • 15
  • 36
11

Here is my favourite example for this problem. I often use it to show Excel's mathematical shortcomings, but not surprisingly it works the same in the C64:

10 A = 0.1
20 B = 0.1
30 FOR I = 1 TO 10
40 D = B
50 B = 20 * A - 19 * B
60 PRINT B
70 A = D
80 NEXT I

In every iteration, the algorithm should be doing 20 * 0.1 - 19 * 0.1 = 0.1, but the output on this simulator is

 .0999999999
 .100000002
 .0999999578
 .100000845
 .0999831052
 .100337895
 .0932421037
 .235157925
-2.60315849
 54.1631699
Martin Argerami
  • 365
  • 2
  • 8
5

Might I suggest you try 0.11+0.12?

I believe IEEE754 will in fact give the right answer on 0.1+0.2=0.3, using standard single precision. It is, however, not difficult to provoke IEEE754 failures, for instance on 0.11+0.12. The C program below show the raw bin32 representations of the relevant IEEE754 numbers, the program output is:

a  :3dcccccd
b  :3e4ccccd
a+b:3e99999a
c  :3e99999a
IEEE754 copes
a  :3de147ae
b  :3df5c28f
a+b:3e6b851e
c  :3e6b851f
IEEE754 fails

Program:

#include <stdio.h>
#include <stdint.h>
int main( void ) {
    float a = 0.1;
    float b = 0.2;
    float c = 0.3;
    float apb = a+b;
    printf( "a  :%x\n", *(uint32_t *)&a);
    printf( "b  :%x\n", *(uint32_t *)&b);
    printf( "a+b:%x\n", *(uint32_t *)&apb);
    printf( "c  :%x\n", *(uint32_t *)&c);
    if ( a + b == c ) {
            printf( "IEEE754 copes\n" );
    } else {
            printf( "IEEE754 fails\n" );
    }

    a = 0.11;
    b = 0.12;
    c = 0.23;
    apb = a+b;
    printf( "a  :%x\n", *(uint32_t *)&a);
    printf( "b  :%x\n", *(uint32_t *)&b);
    printf( "a+b:%x\n", *(uint32_t *)&apb);
    printf( "c  :%x\n", *(uint32_t *)&c);
    if ( a + b == c ) {
            printf( "IEEE754 copes\n" );
    } else {
            printf( "IEEE754 fails\n" );
    }

    return 0;
}
Baard
  • 323
  • 1
  • 6
  • 1
    Ah! I ran the .1+.2 test on a modern machine in double precision; maybe in some sense, single precision is not precise enough to show the difference; CBM BASIC is closer to single precision. But .11+.12=.23 does indeed fail on a C64. – rwallace Jan 27 '19 at 14:04
  • 2
    On every half decent implementation, 0.1 + 0.2 will give you something that is very, very close to 0.3. Whether it equals 0.3 is more or less coincidence. The actual precision doesn't matter, double precision will be much much closer to 0.3, but will also make a lot lot smaller difference "not equal". – gnasher729 Jan 27 '19 at 19:21
  • @gnasher729 Sure... what it equals or not, in the test, is the number produced by the string "0.3", which is 52^54 less than 0.3 in double precision, vs 0.1 + 0.2 being 52^52 greater. – Random832 Jan 29 '19 at 04:25
3

Probably the simplest pattern to look for is 0.1+2-2-0.1. Depending upon precision, 0.1 is going to be (the .... is some number of repetitions of 0011)

0.00011....00110011
 0.00011....0011010
  0.00011....001101
   0.00011....00110

Adding 2 is going to reduce the available precision at the low end by five bits (more than a whole repetition of the 0011 pattern), forcing some kind of rounding there. After 2 is subtracted, zeroes would be shifted into the low-order bits, and subtracting 0.1 would leave values in those bits.

When using decimal floating point, all calculations would be exact, and thus the final residue would be zero.

Incidentally, with regard to speed, the 6502 has patented circuitry to perform BCD addition and subtraction as fast as binary (interestingly, the CMOS versions of the chip do not have such circuitry, causing BCD addition and subtraction to be slower), but performing most other kinds of BCD math efficiently requires some substantial lookup tables.

supercat
  • 35,993
  • 3
  • 63
  • 159
3

Although not a direct answer to the question, I believe this is still worth pointing out:

Atari BASIC used BCD for its FP implementation. It was free from the sorts of errors being outlined above. I have found several other BCD implementations, but nothing "mainstream" to this degree.

The flip side is that BCD is theoretically slightly slower, you have to be careful turning the BCD mode on and off, and you get slightly fewer digits per 40-bit store.

On top of that, the Atari implementation was notoriously slow, but that was not specifically because it was BCD, it was just bad.

Maury Markowitz
  • 19,803
  • 1
  • 47
  • 138
  • 1
    Microsoft BASIC on the MSX, Tandy 100 and Apple Mac were all decimal. The MSX outsold the Atari 8-bits several times over: 9 million (MSX; mostly Japan) vs 2 million (Atari 8-bit, worldwide). – scruss Aug 23 '20 at 19:09
1

Another precision test, previously mentioned in a comment by Tim Locke on this site, was published in Antic magazine Vol 1 No.4. It was submitted by one "R. Broucke" (the late Roger A. Broucke, then at UT Austin):

10 S=0
20 X=0
30 FOR N=1 TO 1000
40 S=S+X*X
50 X=X+0.00123
60 NEXT N
70 PRINT S,X
80 PRINT "CORRECT RESULT: 503.54380215, 1.23"

When run on a binary floating point interpreter (cbmbasic) it produces:

 503.543832            1.23000004

On a decimal floating point interpreter (tibasic):

 503.5438022   1.23 

Typically (but not always) binary floating point interpreters add spurious digits after the 1.23 result. Microsoft produced a few interpreters that used decimal floating point around 1983-4. These include BASIC for the Tandy 100 portable and MSX BASIC. Microsoft BASIC for Macintosh included two interpreters, one binary, one decimal.

scruss
  • 21,585
  • 1
  • 45
  • 113