Someone linked to my earlier article on the problems in Atari BASIC, but that was written long ago and I can offer some additional insights. Curiously, by all rights Atari BASIC should have been quite fast, but a couple of decisions nuked it.
This is going to be long...
To understand where this is going, we need to consider some other examples of early microcomputer BASIC. To do so, we'll consider the following program:
10 A=A+1
20 IF A<1000 THEN 10
When one thinks of "an interpreter" one may think of a system that reads the source line by line (or statement by statement), interprets it, and runs the result. Such systems are rare, but we can consider one example, Tiny BASIC. When the user types the second line in and presses return, you would get something like this in memory:
$14 IF A<1000 THEN 10 $13
At runtime, the interpreter has to read this character by character, figure out each of the keywords, and then run them. Now compare the same line in MS:
$0014 $xx $8B A $B3 1000 $A7 10
MS tokenized the keywords, and at runtime, it can separate them easily because tokens have their high bit set. This short-circuits having to read the text and parse it, at least partially. It still has to parse and lookup variables and convert the numeric constants, 1000 times each. Note the $xx which is a pointer to the next line, allowing it to do line lookups much faster than in Tiny.
Now finally, consider Atari BASIC:
$0014 $xx $xx $07 $80 $20 $0E $430100000000 $1B $410100000000
The line has been completely converted into the form that it will be when it is run. For instance, the $80 indicates the location in memory for the A variable, so there is no need to search for it. The two numeric constants have been converted to their internal format, so again, nothing has to be done at runtime, they just copy it directly into the registers. Additionally, it stores the location of the next line, as well as the next statement, which may or may not be on the same line. This allows IF statements to jump to the next statement without having to search the source for the colon.
So at this point, it would seem AB should run circles around MS. But in fact, it is about 1/3rd the speed on most benchmarks. As I noted in my earlier article, there are two main reasons for this.
The first is that they store all numeric constants in a BCD form. However, the line numbers themselves are in 16-bit int format. This means the line number has to be converted from BCD to a 16-bit int every time through the loop. Had they either (A) provided a second format for storing line numbers, or (B) stored the line numbers in BCD format, all the line lookups would immediately have improved.
Now a GOTO here and there is not going to be too bad, they are not always found in loops. But here's where it goes from bad to worse: they also stored the return line in FOR/NEXT loops as a line number. So every time through a FOR/NEXT, it has to search the entire program for the matching line. This was absolutely brain-dead. There was a patch that came out sometime in the 1980s that made the change to store the address in loops, and it results in an average 50% speedup for about 30 bytes of code.
To a lesser degree, another issue is that the BCD code was complete pants. It's possible to make performant BCD code on the 6502, although multiply and divide will always be slower. But simple stuff like A=A+1 should not be too much different than in binary. Not so in AB, where the code was almost always much slower than the equivalent in MS's binary code. For instance, that BCD-to-int could take some god-awful amount of time.
So in the real world, does the basic idea behind AB - and Sinclair worked the same way BTW - actually improve performance? Well for that we can look at TurboBASIC. TB was a (significantly) patched version of AB which removed some of this dumbness and added a new math package.
The results can be seen here.
Note the faicuai tests, where 100 = 100% of the performance of a C64 - as you can see Turbo runs about 65% faster in this large battery of tests.