Optimizing Color BASIC, part 7

See also: part 1, part 2, part 3, part 4, part 5, part 6, part 7, part 8 and part 9.

GOSUB Revisited

In response to part 4, William Astle wrote a very nice expansion to my musings about INKEY and GOTO versus GOSUB. If you have been following my ramblings, I highly recommend you check out his posting. Unlike me, he actually understands what is going on behind the scenes:

One of the things he pointed out, then explained further in a comment after I didn’t understand, was how the GOSUB processing works. After the GOSUB keyword is found, BASIC acquires the line number and then scans to the end of the line or the next colon. That is where RETURN will RETURN to. This sounds as one might expect, but there was a bit of weirdness I didn’t “get” at first.

William demonstrated that anything after the line number is ignored, thus:

GOSUB 1000 I CAN TYPE STUFF HERE WITHOUT ERROR

…is valid. This surprised me. If you do this:

GOSUB 1000 I CAN TYPE STUFF HERE WITHOUT ERROR:PRINT "BACK FROM GOSUB"

…when the RETURN returns, you will see the “BACK FROM GOSUB” message printed as expected. Anything between the line number and the colon (or start of next line, whichever is found first) is ignored. William explains what is going on in his article.

This, of course, made me do some more stupid testing. First, I modified my benchmark program to run multiple tests and then average out the results. It looks like this:

0 REM BENCH.BAS
5 DIM TE,TM,B,A,TT
10 FORA=0TO4:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 REM
40 REM PUT CODE TO BENCHMARK
50 REM HERE.
60 REM
70 NEXT:TE=TIMER-TM
80 TT=TT+TE:PRINTA,TE
90 NEXT:PRINTTT/A:END

Then I reran my GOSUB test:

0 REM GOSUB3.BAS
5 DIM TE,TM,B,A,TT
10 FORA=0TO4:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 GOSUB100
70 NEXT:TE=TIMER-TM
80 TT=TT+TE:PRINTA,TE
90 NEXT:PRINTTT/A:END
100 RETURN

When I run this, it prints the time taken for each run, and then the average:

GOSUB benchmarks.

Now for the stupid test, I added some junk after “GOSUB 100” and filled it up to the end of the line.

Side Note: I am loading BASIC programs in ASCII, so the program lines load in as if they were being typed in. Thus, it counts the characters “100 GOSUB ” as part of it. But, as soon as you press ENTER, that line is tokenized and GOSUB becomes a 1-byte token (is it 1-byte?). Then you can EDIT the line and Xtend it and type in a few more characters. So what I show here isn’t the max line size, but it is the max line size I could load in from an ASCII BASIC file. But I digress.

My line looks like this:

30 GOSUB100 ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRST*

Now when I run this, the extra “scan to the end” time causes the benchmark to show 1507!

But who would do that? If anything, you would have a colon and real stuff after the GOSUB. So I tried this by changing the space after “GOSUB 100” to a colon and “REM”:

30 GOSUB100:REMABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOP*

Now that’s a completely legitimate line. (Pretend the ABC/123 gibberish is a really long comment.)

This benchmark shows 1508, so no real difference. When GOSUB is encountered, BASIC has to scan to the end of line or a colon, whichever comes first, so it should find the colon instantly, BUT, after the RETURN it still has to scan through that REM to find the next line. Thus, it’s the same amount of scanning.

This is a meaningless test.

With real code, you might be doing something like this:

30 GOSUB 100:PRINT "BACK FROM ROUTINE"

Or you could have written it out as two lines:

30 GOSUB 100
40 PRINT "BACK FROM ROUTINE"

I thought the first one should be faster, since it has one less line.

And combining lines is good.

Right?

Well, in my silly example #2 above, what if I moved the REM to the next line, like this:

30 GOSUB100
40 REMABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQRST*

That’s basically the same, just with an extra line number.

When I run this, I get a benchmark value of … 860!

Look how much faster it is by moving code to a separate line! I guess we should use separate lines after all, then…

What’s going on here? The key seems to be the “REM” keyword. When BASIC encounters a REM, it can just skip to the next line. That makes it faster. But, it seems to be doing something different when a REM is in the middle of a line.

It appears it is faster to NOT put REMarks after a GOSUB.

30 GOSUB 100:REM MOVE PLAYER UP

…shows 266. This is slower than…

30 GOSUB 100
40 REM MOVE PLAYER UP

…which shows 220. And I’ve certainly seen programmers make use of the apostrophe REMark shortcut.

The apostrophe represents “:REM” (colon REM) so these two are the same:

30 GOSUB 100:REM MOVE PLAYER UP
30 GOSUB 100' MOVE PLAYER UP
REM versus ‘ for comments.

Thus, using the one character apostrophe may look like it saves code space versus “:REM” but it does not. It does save printer paper, though :)

But I digress…

It looks like I’m going to need another test. In the meantime, don’t put things after a GOSUB om the same line. It appears to be faster to put them on the next line:

30 REM MOVE PLAYER UP
40 GOSUB100

That is 219.

30 GOSUB100:REM MOVE PLAYER UP

That is 266!

30 GOSUB100
40 REM MOVE PLAYER UP

That is backwards. But it produces 220, so it doesn’t penalize you for being backwards.

Oh, and as Steve Bjork pointed out in the Facebook group, a faster solution is not to use REMs at all. I think I need smarter examples. There are too many real programmers watching. For you folks:

0 REM THIS TAKES 371
5 DIM Z,TE,TM,B,A,TT
10 FORA=0TO4:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 GOSUB100:Z=Z+1
70 NEXT:TE=TIMER-TM
80 TT=TT+TE:PRINTA,TE
90 NEXT:PRINTTT/A:END
100 RETURN
0 REM THIS TAKES 357
5 DIM Z,TE,TM,B,A,TT
10 FORA=0TO4:TIMER=0:TM=TIMER
20 FORB=0TO1000
30 GOSUB100
40 Z=Z+1
70 NEXT:TE=TIMER-TM
80 TT=TT+TE:PRINTA,TE
90 NEXT:PRINTTT/A:END
100 RETURN

Easy peasy.

A few additional REMarks…

Above, I mentioned that the apostrophe represented “:REM”. Thus, doing something like this:

100 'MOVE UP

Is slower than doing:

100 REM MOVE UP

It may look smaller, but the first example is like scanning “:REM MOVE UP” and the second is just “REM MOVE UP” so it has less work to do.

And yes, I tested it inside the benchmark program:

30 REM

…is 82.

30 '

…is 90.

30 :REM

…is also 90.

I guess it’s just treating the apostrophe as “:REM” internally, or maybe it’s a 2-byte token for “:REM” versus a different 1-byte token just for “REM” or something. Dunno.

But interesting.

Until next time…

5 thoughts on “Optimizing Color BASIC, part 7

  1. William Astle

    GOSUB and GOTO are both “two byte” tokens. Actually, “GO” is one token and then TO/SUB are separate tokens. I think this is a throwback to the original Basic which actually specified GOTO as “GO TO” (two words). By separating it into two tokens, it allows the two word variant for free due to the “skipping spaces” effect of the “next character” routine.

    On the REM vs ‘ business: ‘ does have its own distinct single byte token. The tokenization process adds a : before it during tokenization behind the scenes which is why you don’t need a : before ‘ but you do before REM. Otherwise it is treated exactly the same as REM during interpretation so :REM and ‘ execute at the same speed. You can see this detail by examining the tokenized basic program. Thus, if your comment is at the start of the line, you’re better off using REM and if it’s at the end of the line, it makes no difference.

    As a bit of trivia, the tokenizer also adds a colon before ELSE which is almost certainly done as an optimization so the execution process doesn’t have to look for “ELSE” as yet another “end of statement” indicator.

    Even more trivial: you can replace “THEN” with “GOTO” or “GOSUB” directly. There’s no benefit to doing that for GOTO, but it saves a byte with GOSUB.

    Reply
    1. Allen Huffman Post author

      So in the token, a : is stored before the ‘ token? Does this mean the detokenizer that is used for LIST has code to not print the :’s it finds before ELSE and ‘?

      Reply
  2. carl england

    THEN GOTO can be replaced by either GOTO or THEN saving either 1 or two bytes. THEN GOSUB can only be replaced by GOSUB resulting in a 1 byte saving. The CRUNCH program replaces THEN GOTO with THEN, and THEN GOSUB with GOSUB. (It doesn’t replace either if there is a space between THEN and either GOTO or GOSUB.) *Note* GOTO can be expressed as GO TO and GOSUB can be expressed as GO SUB.

    Reply
  3. Pingback: BASIC and ELSE and GOTO and Work – part 2 | Sub-Etha Software

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.