I have spent a good chunk of this year squeezing two games onto a selection of vintage hardware. A Roguelike dungeon crawler and a Star Trek inspired game that started life as 1970s BASIC. Both have to run on machines with less RAM than a modern device uses to draw a single emoji.

Something that surprised me, and was the inspiration behind writing this up, is that BASIC will often let you build a bigger-feeling game than C will, unless you are very, very careful with the C.
That feels wrong.
C is a “serious“, lower-level systems development language, and one that compiles to fast native code. So why does the BASIC version sometimes fit where the C version won’t?
The answer sent me on a mystery tour through where memory actually gets consumed on these machines, and how interpreted BASIC is different to compiled C code. I am sure someone will point out all of this is a skill issue on my part, but I imagine there are other people like me out there, so here we go …
BASIC versus C, and why the “tortoise” succeeds
When you type a BASIC program into a C64 or an Amstrad CPC, almost everything that makes it work is already in the machine. The interpreter, the maths routines, the string handling, the screen driver, all of it is sitting waiting to be accessed in ROM. Your program is stored as tokens, so PRINT is a single byte rather than five characters. You are essentially writing a thin script on top of a runtime that you already paid for.
C is often the opposite. We try to fit into RAM everything the ROM was giving us in BASIC for free. Every routine gets linked into our bloated binary. Buffers we declare sit initialised in RAM taking up space whether or not they are full or even used. There is no friendly garbage collection worker quietly housekeeping things behind our back. Yeah we get speed and control, but in exchange you pay for every byte.

Why not use the ROM routines in C then? Of course that is something we do, but when working cross-platform we come across another issue. A big point of using C instead of raw assembly is the benefit of a portable codebase. We want to be running running as much of the same, generic as possible, source code across a dozen different architectures, some of which might have no BASIC ROM at all. We want to use printf, for example, not MOV AX .
On top of that, the really useful ROM routines (floating-point maths, string handling, and so on) expect their arguments in fixed places that compete for the very memory the C runtime needs. So portable C ends up reinventing its own maths and string code, partly out of necessity and partly to stay portable. Every additional routine gets linked into your binary, and the buffers you declare sit in RAM whether or not they are full.
So there is a real but sneaky tradeoff. BASIC is slow but doesn’t care if you use lots of strings. C is fast but that isn’t going to help you if your code is verbose.
Strings are especially hard
Strings are the clearest example of this paradox. In BASIC, a string is a dynamic thing. The interpreter allocates it on the fly, grows it, copies it, and is ready to tidy up when you make a mess. A$ = A$ + "!" just works. It will freeze gameplay to collect garbage at the worst possible moment, but it does work, and it costs you almost nothing.
In C, especially on an 8-bit target, you usually have none of that unless you build it yourself. A string is a fixed block of memory you specifically reserved up front. If you want to hold a name that might be up to sixteen characters, you reserve sixteen bytes (plus one), and those bytes are owned for the life of the program. Do that for a few dozen game status messages, a HUD line, a couple of input buffers, and a menu, and you have quietly spent a kilobyte or two before any actual game logic runs. There is no garbage collection to rely on, because you didn’t make one.

BASIC’s string handling is expensive in terms of performance, as we have seen many times on this blog, but is super generous when it comes to space. C, on the other hand, wants the entire bill paying up-front.
Where RAM actually goes
When a C program refuses to fit, the temptation is to stare at your code and wonder why so few lines produce so large a binary. But the binary on disk is only part of the story. There are several places memory gets gobbled up, and most of them are invisible to the source files.
- Code: Your compiled functions and logic. On my Trek-alike the integer only game logic came to roughly 20 to 24KB of code, depending on the target. That is before any data.
- Initialised data and BSS: BSS (“Block Started by Symbol“) is the region for variables to live. Your global arrays, the big game-state structures, those fixed string buffers, and so on. It does not take up space in the file on disk, which lulls you into a false sense of security, but it absolutely does take up RAM when the program runs. On the Z80 build the read-only data and zeroed BSS added close to 12KB on top of the code, pushing the whole image to around 33KB. The Amstrad CPC has only about 32KB usable below its ROM, so it spilled over the edge before the stack even got its slice, and no amount of clever code shaving was going to claw that back.
- The stack: Every function call, every local variable, every nested bit of logic uses stack. The catch is that the stack and your statically allocated buffers grow towards each other from opposite ends of memory like a train crash waiting to hit. I did hit exactly this on the CPC as I had set the data start address so high that there were only about 3840 bytes left for data, BSS and stack combined. The static buffers ate nearly all of it, the stack had almost nowhere to grow, and the program booted to a perfectly blank screen. It started, drew nothing, and sat there. It had compiled fine, but compilation and executing correctly are two very different milestones. That is the kind of bug that costs you an entire evening of hair pulling, because everything “should work” right up until it silently does not. The fix was moving the data start address to give the stack room to breathe.
- Zero page: On 6502 machines (the Commodore, Atari, AppleII, and Acorn friends) there is a tiny, special, fast region called zero page. It is kind of like having a whole bunch of registers, and when you learn it can be gorgeous to work with until there is almost none of it left, and the ROM and the C runtime both want a large slice. You do not so much allocate zero page as wrestle for it.
The lesson from all of this is that “will it fit” is not a question you can answer by looking at the code or even the hardware specs. I ended up building a little memory audit tool in python precisely because I needed to see the resulting segment map. How much went to code, how much to BSS, which module was the glutton, and how much headroom was actually left. Eyeballing it does not work once you are within a few kilobytes of crashing out.
| Machine | CPU | RAM budget | Code | Read-only data | BSS (zeroed) | Headroom | Fits? |
|---|---|---|---|---|---|---|---|
| Commodore PET | 6502 | 31,251 | 24,132 | 2,295 | 2,566 | 2,107 | Yes, only just |
| Commodore 64 | 6510 | 49,139 | 24,284 | 2,253 | 2,568 | 19,868 | Yep |
| Commodore 128 | 8502 | 39,923 | 24,222 | 2,255 | 2,568 | 10,687 | Yes |
| ZX Spectrum | Z80 | 41,472 | 20,926 | 3,279 | 8,660 | 8,584 | Yes |
| VIC-20 (stock) | 6502 | 1,523 | 24,401 | 2,253 | 2,593 | none | No, miles over |
Look at the two CPU families side by side. The 6502 builds show about 24KB of code but only around 2.5KB of zeroed BSS. The Z80 build is the other way round with less code, about 21KB, but its BSS balloons to over 8KB, because that toolchain reserves much chunkier string buffers up front.
Same game source, completely different graph of memory use. It is also why the Amstrad CPC, a “64KB” Z80 machine with only about 32KB usable below its ROM, is the awkward one. Roughly 21KB of code, plus 3KB of read-only data, plus nearly 9KB of BSS puts you right at the ceiling before the stack even gets a look in.
One number in that table looks like a mistake. The C128, a machine with supposedly twice the memory of a C64, shows the smaller budget. What??
The figure is not the machine’s total RAM, it is the single contiguous block the compiler links into by default. On the C64 the toolchain banks out the BASIC ROM and runs RAM all the way up to the input and output chips at $D000, which gives about 49KB in one unbroken stretch. On the other hand, the C128 starts its block roughly 7KB higher up, because it reserves more low memory for BASIC 7.0, the memory management unit that does the banking, and the screen editor. It also stops lower down, leaving the ROM and MMU region above $C000 alone. The C128’s extra 64KB is there, but it lives in other banks. Bank switching or a custom linker config would reach it, but the smaller figure is the default contiguous one. This also explains why an unexpanded (5KB) VIC-20 shows barely 1.5KB. It’s because our budget is the usable region you get out of the box, not the number on the box.
Cartridge versus magnetic storage
There is one more trick up our sleeves, and it changes the situation entirely … where the program lives when it is not running.

If you ship on tape or disk, the program has to load into RAM and stay there. Code and data are fighting over the same space the running game needs. That is the situation above, where 33KB simply will not go into 32KB. You can trade speed for space by “lazy loading” data, for example loading the next map from disk, but you still have to walk that game logic tightrope.
A cartridge is different. The code can live in ROM on the cartridge and execute from there, which means it is not eating your precious RAM at all. Suddenly that 21KB of game code is not competing with your data and stack, because it never takes up RAM in the first place. RAM is freed up to be what it should be, working space for the game state, not storage for the program.
This almost invalidates the “C is too big” problem, because if you deploy it it on a cartridge, the same program is quite comfortable. On many systems back in the day, the games that pushed these machines hardest shipped on cartridge for exactly this reason. One wrinkle, though, is cartridge formats. It works theoretically that your game logic could be any size, but only if the machine doesn’t limit your choices. If the machine dictates 16KB maximum for game cartridges, that becomes your limit.
Next Steps and Current status
Both now compile (and execute!) cleanly across the main 8-bit CPU families, 6502, 6809 and Z80, Atari ST + Amiga, plus runs native on my Mac, Windows, and Linux terminals. This means my cross-platform text-mode stuff is solid so far as the current requirements and target systems. The dungeon game even works on Dragon 32 and Vic 20 (albeit expanded)!
But then as well as getting the games to compile and execute, they also have to be fun to play. Part of that is making them usable in different screen modes, for example Vic 20 has a much smaller available screen size than an x86 SVGA or even a CP/M terminal. Bigger screen isn’t necessarily better, though, because on some computers the luxury of colour comes at the expense of too high screen RAM cost.

Hitting the limits of the target machines means fewer systems will get the full experience, but all along I have been expecting that and it is why things have been so slow. Trying to make a great, or at least working, baseline and then embellish for those that have the additional room. I have to admit I thought it would be more a speed decision than features at this point in the process, but it is what it is.
The CPC remains the awkward child, and it is awkward for all the reasons above. Space below ROM, hungry data segment, a stack that needs vigilance … It’s weird because the Amstrad CPC is supposed to have lots more capacity than the Speccy or Dragon by default, and yet I didn’t have to fight anywhere near as hard on those as the CPC.
If there is one big lesson learned, it is this:
C gives you speed and control, but it demands you manage all the stuff BASIC does on your behalf. Planning and keeping track of where every byte goes, code, BSS, stack and zero page, and knowing whether you can hide the code in a cartridge, matters far more than how cycle-elegant your game loop is!


C64IDE: Powerful, Free Mac Commodore 64 IDE Review