Why do C to Z80 compilers produce poor code? (2018)
retrocomputing.stackexchange.com
retrocomputing.stackexchange.com
I see everyone there seems to be referring to free/open-source compilers, but I know of one commercial compiler for Z80 (IAR) whose output is quite good, and I have linked to in this previous comment I made on a different article about Z80 C compilation:
https://news.ycombinator.com/item?id=18902501
If one didn't know better, it would look almost like handwritten Asm.
On the topic of architectures which are difficult to compile C to, I can also think of several more: 8051, low-end PIC series, and these: https://cpldcpu.wordpress.com/2019/08/12/the-terrible-3-cent...
...and most of those have C (or at least C-subset) compilers too.
Because saving and restoring register values is fairly expensive (both in time and in memory) you shouldn’t do register allocation per function, or commit to a fixed ABI. The best assembly to generate for a function often will depend on what it calls or where it is called from. Let’s say you need a register or flag to be clear at some time. A function you just called might accidentally already do that, or it might be cheaper to change that function’s code or register allocation to give it the desired side-effect than to have the function that needs the register or flag cleared waste an entire byte to do that.
You basically need a whole-program (compiler-linker) to generate good code for these CPUs.
That also helps in moving local variables to global addresses (you likely will have to, because of limited stack space) and in having such variables share memory locations as much as possible (if foo doesn’t (indirectly) call bar and vice versa, ant hey aren’t recursively called, their local variables can share the same memory addresses. Again, you’ll likely have to, because memory is limited)
Modern compilers can do this, 'link time optimisation'.
I also doubt they’ll use status flags to return data from functions. Such micro-optimizations just aren’t that useful on heavily pipelined CPUs with zillions of registers.
But yes, a link-time optimizer would work, but it would have to be built for these architectures, not, as in gcc’s or LLVM’s case, for the intermediate language.
Edit: for stuff that I don’t see a traditional link time optimizer do, look at CHRGET and CHRGOT in http://unusedino.de/ec64/technical/project64/mapping_c64.htm....
Semi-equivalent C code (leaving out returning info in the status flags) is:
void * addr;
void * CHRGOT() { return addr; }
void * CHRGET() { while(shouldSkip(++addr)); return addr;}
The assembly code uses self-modifying code for speed, and makes CHRGET fall-through into CHRGOT, adding an unnecessary while-loop to CHRGOT.Also, there’s the choice as to what flags to use for returning info.
https://github.com/z88dk/z88dk/wiki/Benchmarks
Among IAR, there's also HITECH C Compiler, which, when it is not disqualified for bad results, seems to have some impressive numbers at times. (And the project's own C compiler, when it does not hit a pathological case.)
The fact is that there are/were some very effective C compilers for Z80, none of them free and far from it. By the time compiler technology advanced through the 80s and 90s the market for a non commercial optimizing compiler for the Z80 and 6502 rapidly diminished. gcc has never in its history been a fantastic 8 or 16 bit compiler. By the time egcs was merged even 68k was rapidly losing relevance.
It is totally possible with modern techniques to make a very effective optimizing C compiler for Z80, but who’s going to do it? There are very few hobbyists with both the chops and the time for something with little commercial value.
The answer is correct in implying that the PDP was better suited as a CISC target for C when optimization is not done.
[0] https://github.com/siraben/ti84-forth/blob/bc0140d641a121a4f...
https://en.wikipedia.org/wiki/Jupiter_Ace
Also, there is an obscure french Z80 computer that uses Forth : Hector HRX
That allowed you to write non trivial programs in C for the 8031/32 without external RAM.
Edit: Wow, as a result of your other comment about GCC I was surprised to learn that none of the Z80 compilers I could find appear to support this sort of thing. It looks like there was (previously?) an effort to put together a Z80 backend for LLVM, but my search results didn't look promising.
Also, it is not clear that the thread is still on the topic of z80 specifically or whole-project LTO generally. If you find yourself doing the latter outside of z80, you may find GCC useful. And hidden visibility is often useful a good default.
If you want a z80-specific LTO option, sorry, I'm unfamiliar with any. That's a pretty niche ask.
Those options you gave are GCC-specific.
I don't know of a mature Z80 compiler that supports LTO.
The thread at this point is on the topic of "so your compiler doesn't support LTO, how can we 80/20 our way to the same semantics"
(PL/M targets, like CP/M, the 8080 instruction set, of which the Z80 provides a superset.)
Chuck Moore has claimed "A Forth application can take 1% the code employed by the same application written in C" [1], which is certainly a big benefit on a small computer.
Most of the TI-83/84 scene was hand-written z80 assembler.
Anyone that was writing code where performance mattered was using Assembly.
Even on 16 bit platforms, although high level languages were already quite common, in what concerns games (or demoscene), they were our "Unity".
Good enough for prototyping, but also full of inline assembly.
I once saw a game submission to a Portuguese newspaper (for a MS-DOS game), which the only C that it had were data structures and function declarations, the bodies were 100% inline Assembly.
And by the time Pentium arrived, unless one was buying Watcom, there wasn't a good C / C++ compiler that could take advantage of the pipelines and instruction scheduling in a proper way, as described by Abrash books.
The "Action!" programming language cartridge was the best thing ever. Somewhere between C and machine language. Being in ROM it booted instantly and contained a built-in editor, compiler, runtime, and monitor. It compiled about as fast as Go does now on modern hardware.
It wasn't until I got an Atari ST (Megamax was good) or PCs that C I got to use C compilers at home.
[0] https://en.wikipedia.org/wiki/Action!_(programming_language)
Here’s an open source game (disclosure: I wrote it) that compiles with SDCC and runs a game involving a flood fill algorithm, a tiny GUI with checkboxes and buttons and only one assembly routine, all running on the Amstrad CPC464 and above: https://github.com/cpcitor/color-flood-for-amstrad-cpc
It’s much easier to write and much faster (and probably smaller) to run than the same in BASIC.
Nineties style.
You can git clone the project above in a Debian/Ubuntu or similar, do "make" and have a look at the .lst listing of generated assembly with opcode bytes.
I consistently observed that while z88dk produced extremely verbose code, with many steps back and forth and many extra push and pop, SDCC produced sane code.
Of course it helps a lot when you "const" all relevant variable, and "const type const (star)" all relevant pointers when applicable, when you use uint8_t instead of int. Else generated code is heavy because it pays the code of high genericity that is probably not necessary. Help the compiler and, if it's a good compiler, it will help you. SDCC is a good compiler in this regard. This is especially important in the Z80 context because registers are smaller than the default integer types.
I observed good code produced by SDCC. Too long to explain here but for example it allocates local variables to registers when applicable and code is somehow close to hand-written code (like in answers of the SO question).
For example, it replaces a memset() C function call with this code which fits what one SO answer calls "kind of code one writes where optimization does not matter", which z88dk could not do:
243 ;model.c:132: memset( &connections, 0, sizeof( connections ) );
00A2 21r68r00 [ 3] 244 ld hl, #_connections
00A5 06 19 [ 2] 245 ld b, #0x19
00A7 246 00244$:
00A7 36 00 [ 3] 247 ld (hl), #0x00
00A9 23 [ 2] 248 inc hl
00AA 10 FB [ 4] 249 djnz 00244$
(you might have zeroed a and done ld (hl),a, or even ldir, but this is still correct code)The next one below looks like it's easy. It is, if the compiler can figure out what is constant, and because you write sane C code. Garbage C code would yield garbage ASM code.
Line uint8_t column = 20 - ( sizeof( message ) - 1 ) / 2; below yields no code, zero bytes. push de is the C ABI
Code: [Select]
63 ;uint8_t column = 20 - ( sizeof( message ) - 1 ) / 2;
64 ;testfixture.c:23: fw_txt_set_cursor( 2, column );
4058 11 02 03 [ 3] 65 ld de, #0x0302
405B D5 [ 4] 66 push de
405C CD 43 41 [ 5] 67 call _fw_txt_set_cursor
Another Z80-specific consideration: Z80 is not made to access data off a stack. From the 8080 it added IX and IY index registers. You can `ld someregister, (ix-+127)` and family. They are somehow handy (somehow short code) but slow. SDCC uses this extensively.Chart of Z80 instruction timings: https://wiki.octoate.de/lib/exe/fetch.php/amstradcpc:z80_cpc...
Z80 is more comfortable with all-fixed-addresses code. In a function, making local variables static kills the reentrancy but then you can replace the slow `ld someregister, (ix-+127)` with faster and shorter `ld a,(someaddress)`. I observed 20% reduction in SDCC-generated code in a big algorithmic function, which is attributable both to Z80 constraints and ability of SDCC somehow navigate those decently. In complex functions, code generated by SDCC is not extremely nice, but it's decent. It'd good enough so that only the performance-critical parts have to be written in assembly.
On another side, SDCC supports most (all?) of C11. I've tested it, you can e.g. print(var) like you'd stdout << var in C++ and let the compiler do what you mean. Unusual in a microcontroller context.
I tried to do that a couple years ago with little success (readme and feature list missing, sorry but maybe you get the idea from the repo): https://github.com/NanoWar/FunkCompiler