Switch on a Commodore 64 and it reports 38911 BASIC BYTES FREE. Every program, and every number and string the program uses, has to fit in that space, and BASIC decides how. It packs the program in at the bottom, builds a table of variables after it, and keeps string text at the top, working downwards. Each of those choices has a cost a BASIC programmer could feel: names that clash, memory that vanishes, and a game that freezes for seconds while the interpreter tidies up its strings.
The ZX Spectrum’s BASIC solved the same problems another way. Comparing the two shows what each machine was doing behind the READY. and the 0 OK.
The Commodore 64’s map
BASIC V2, Commodore’s version of Microsoft BASIC, keeps the boundaries of its memory in pairs of bytes in zero page, each a pointer stored low byte first:
| Locations | Name | Points to |
|---|---|---|
| 43–44 | TXTTAB |
Start of the program, normally 2049 ($0801) |
| 45–46 | VARTAB |
Start of the simple variables, just after the program |
| 47–48 | ARYTAB |
Start of the arrays |
| 49–50 | STREND |
End of the arrays, and start of free memory |
| 51–52 | FRETOP |
Bottom of the string text, which grows downwards |
| 55–56 | MEMSIZ |
Top of BASIC’s memory, 40960 ($A000) after switch-on |
Free memory is the gap between STREND and FRETOP. New variables and arrays push it from below and new string text pushes from above. PRINT PEEK(55)+256*PEEK(56) gives 40960 on a freshly started machine. Above that address sits the BASIC ROM.
Each program line is stored as a two-byte link to the next line, the two-byte line number, the line’s text with each keyword squeezed into a one-byte token, and a zero. 10 PRINT "HELLO" takes 14 bytes: 4 for the link and line number, 1 for the PRINT token, 8 for the space, the quotation marks and HELLO, and the final zero.
Seven bytes a variable
Every simple variable takes seven bytes in the table that starts at VARTAB: two for the name and five for the value. A floating-point number fills all five. An integer variable such as A% stores its value in two of them, high byte first, and wastes the other three. As Sheldon Leemon pointed out in Mapping the Commodore 64, “nonarray integer variables take up no less space than floating point variables”, and because BASIC converts them to floating point to work with them, they are usually slower too.
The name bytes hold the first two characters of the name, with the top bit of each used to mark the type. You can see the record by PEEKing it:
10 A$="HELLO"
20 A%=10
30 V=PEEK(45)+256*PEEK(46)
40 PRINT PEEK(V);PEEK(V+1);PEEK(V+2);PEEK(V+3)+256*PEEK(V+4)
50 PRINT PEEK(V+7);PEEK(V+8);PEEK(V+9);PEEK(V+10)A$ is the first variable created, so its record starts at the address in 45–46. Line 40 prints 65 128 5 2057: an A, a second byte of 128 (no second letter, with the top bit set to mark a string), the length 5, and the address of the text. Line 50 prints 193 128 0 10 for A%: both top bits set mark an integer, then 10 as a high byte and a low byte. A DEF FN function sets the top bit of the first byte only.
Only those two characters count. SCORE, SCREEN and SCROLL are all the variable SC. Worse, BASIC turns any keyword it finds inside a name into a token as the line is typed, so SCORE=100 becomes SC, the token for OR, and E=100, and stops with ?SYNTAX ERROR. The Programmer’s Reference Guide warns that names “may NOT contain keywords in the middle”.
BASIC looks a variable up by searching the table from the start, in the order the variables were created, so Leemon advised creating the busiest ones first. And because the arrays sit straight after the simple variables, “every time a nonarray variable is created, all of the arrays must be moved seven bytes higher in memory”.
Strings and the garbage problem
A string variable’s seven bytes hold a three-byte descriptor: the length and a pointer to the text. The text itself can be in one of two places, and the difference matters.
A string written into the program as a constant is not copied anywhere. Microsoft’s own source code for the 6502 BASIC explains that assigning one “WILL NOT USE ANY STRING SPACE SINCE THE STRING DESCRIPTOR WILL POINT INTO THE PROGRAM ITSELF”. That is why the 2057 above is an address inside the program: line 10 starts at 2049, and the H of HELLO is the ninth byte of the line. After that program has run, FRETOP still reads 40960. No string space has been used.
Any string that is built rather than written, by the + operator, by string functions such as CHR$, STR$ and MID$, or by INPUT, gets new space at the bottom of the string area. Running B$=A$+"X" straight afterwards moves FRETOP down by six bytes, to 40954. When a string variable gets a new built value, the old text stays where it was, now unreachable: garbage. Over a game’s run, the string area creeps down towards the arrays.
When they meet, BASIC collects the garbage, and on the C64 that is slow. The routine looks through every string descriptor for the string highest in memory, moves it to the top, and then starts the search again for the next one, until every live string has been moved. Leemon warned that with more than a few strings, “scanning every string as many times as there are strings can take an awful lot of time. The computer may seem as if it had died (the STOP key is not even checked during the procedure).” Because each pass searches every descriptor again, the work grows with the square of the number of strings, and Leemon warned that “a large program with many string arrays may still experience lengthy collection delays”, at a moment the program does not choose. The defence is to build few strings: text written as constants in the program costs no string space at all.
FRE(0) reports the free memory, and it calls the garbage collector first, so it is also a way to choose when the pause happens. It has a quirk of its own: it treats the answer as a signed 16-bit number, so on a C64 with more than 32,767 bytes free it comes out negative. The manual’s advice is to “add 65536”.
Arrays
An array has a header of five bytes plus two for each dimension, then its elements: five bytes each for floating point, two for integers and a three-byte descriptor for strings, whose text is stored separately like any other string. DIM A(10) has eleven elements, 0 to 10, so it takes 5 + 2 + 55 = 62 bytes.
| Array | Calculation | Bytes |
|---|---|---|
DIM A(10) |
5 + 2 + 5 × 11 | 62 |
DIM A%(10) |
5 + 2 + 2 × 11 | 29 |
DIM N$(10) |
5 + 2 + 3 × 11 | 40, plus the text |
DIM G(20,20) |
5 + 4 + 5 × 21 × 21 | 2,214 |
Integer arrays are where % earns its keep: they take two-fifths of the space of floating-point ones, for any whole numbers from −32768 to 32767.
Fencing off memory
Machine code, character sets and sprite data need memory that BASIC will not overwrite. The usual answer is to lower MEMSIZ, the top of BASIC’s memory, then use CLR to reset the variables and the string area below the new limit. Leemon’s example sets aside 1K with POKE 56,PEEK(56)-4:CLR. The Programmer’s Reference Guide does it before moving the character set to 12288: POKE 52,48:POKE56,48:CLR sets the high bytes of the string pointer and the top of memory to 48, putting both at 12288 ($3000), after which “BASIC now thinks it has less memory to work with”. Run it at the start of a program, since CLR throws away every variable.
The 4K block from 49152 ($C000) is outside BASIC’s memory altogether, above the BASIC ROM, which is why so many machine-code routines were loaded there.
Talking to machine code
SYS jumps to machine code, and the Programmer’s Reference Guide says that parameters “are generally passed … using the BASIC PEEK and POKE statements”. Locations 780 to 783 make it easier: before every SYS, BASIC loads the processor’s A, X, Y and status registers from them, and afterwards it stores the registers back. POKE 780,65:SYS 65490 puts 65 in the accumulator and calls the KERNAL’s character output routine, which prints an A. USR(X) works differently, passing its argument in the floating-point accumulator at location 97 ($61) and returning whatever is left there.
A machine-code routine can also find a variable itself. If A$ was the first variable created, its descriptor is at VARTAB+2:
100 A$="TEST"
110 V=PEEK(45)+256*PEEK(46)
120 L=PEEK(V+2):AD=PEEK(V+3)+256*PEEK(V+4)
130 FOR I=0 TO L-1:PRINT CHR$(PEEK(AD+I));:NEXTThis prints TEST, reading the length from the third byte of the record and the address from the fourth and fifth.
The Spectrum’s way
Sinclair BASIC has no string heap. A string variable is stored whole in the variables area, which starts at the address in the system variable VARS (23627): a byte for the letter, two bytes for the length, then the text itself. The manual’s chapter on memory lays out every format. A number with a one-letter name takes six bytes, the letter and a five-byte value; a longer name stores every character, because numeric names on the Spectrum can be any length and all of it counts. A FOR loop’s control variable carries its value, limit and step and the line and statement to loop back to, 19 bytes in all. String names, array names and loop variables are limited to one letter.
When a string changes, the ROM does not leave the old text behind. The Complete Spectrum ROM Disassembly shows LET making room for the new string at the end of the variables area, then reclaiming “the whole of the existing version”, which closes the gap by moving everything above it down. So a Spectrum never needs to collect garbage and never freezes for it. The cost is spread out instead: every string assignment moves part of memory, and the more variables follow the string, the more bytes move.
The effect is easy to see. After LET a$="hello" and LET v=PEEK 23627+256*PEEK 23628, the first byte at v is 65, the code the Spectrum uses for a string named a. After LET a$="hi" the first byte there is 118, the numeric variable v, and a$ has moved to six bytes later, its length now 2. Meanwhile LET score=100 works as you would hope on a 48K Spectrum: its keywords are entered as single tokens from the keyboard, not picked out of what you type, so a name can contain OR.
Saving bytes
Commodore’s own manual has a section on “crunching” programs, and its advice follows from the storage rules:
- Spaces cost a byte each, so remove them.
REMlines take room in memory, though they are useful in a copy you keep for editing.- Numbers in the program text are stored as their digits, so a number used many times is cheaper in a variable:
POKE V,15instead ofPOKE 54296,15. The same applies to line numbers inGOTOandGOSUB, which are stored as digits, although each line’s own number always takes two bytes. - Abbreviated keywords save nothing in memory:
?andPRINTare the same one-byte token. What abbreviations do is let you fit more on one 80-character screen line as you type it.
Sources
- Commodore, Commodore 64 Programmer’s Reference Guide (1982): pp. 5–8 (number storage, variable names and array sizes), pp. 24–26 (crunching), pp. 48–49 (
FRE), pp. 108–109 (POKE 52,48:POKE56,48:CLR), pp. 306–307 (SYSandUSR), pp. 310–318 (zero page and page 3 memory map). - Sheldon Leemon, Mapping the Commodore 64 (COMPUTE! Publications, 1984): pp. 15–17 (locations 43–56: program, variable, array and string storage), pp. 107–108 (
FREand the garbage collector), locations 780–783 and theSYSroutine at 57642. - Microsoft, BASIC M6502 8K version 1.1 source code, published on GitHub under the MIT licence in September 2025: the description of string storage, the garbage-collection algorithm and the 1978 change log. https://github.com/microsoft/BASIC-M6502
- Steven Vickers, Sinclair ZX Spectrum BASIC Programming, edited by Robin Bradbeer (Sinclair Research, 1982; second edition 1983): chapter 7, p. 45 (variable names), chapter 12 (array names), chapter 24, pp. 166–167 (program and variable formats).
- Sinclair ZX Spectrum +2 manual (Amstrad, 1986): the
VARSsystem variable. - Ian Logan and Frank O’Hara, The Complete Spectrum ROM Disassembly (Melbourne House, 1983), pp. 151–155: the
LETroutine, L-ADD$ and L-STRING.
Released source code
Microsoft published the source code of its 6502 BASIC, version 1.1, the family the Commodore 64’s BASIC V2 comes from, on GitHub under the MIT licence in September 2025. Its comments describe the string descriptors and the garbage collector in Microsoft’s own words, and its change log records a fix dated 1 July 1978 for a bug “WHERE GARBAGE COLLECTION NEVER(!) COLLECTS TEMPS”, noted as “COMMODORE BUG #2”.