59

I found Why do C to Z80 compilers produce poor code? very interesting as it pointed out that C (which was leveraged to be an abstraction of a CPU for porting Unix) was not a very easy language to create efficient machine code from for the Z80. Apparently the same for 6502 where many dive directly in machine code. I read that the Sargon chess engine was very well suited for the 6502 due to the X and Y index registers.

I know that the Z80 and the 6502 are very different, but I was wondering if there are any languages on a higher level than assembly which can generate compact and efficient 8-bit machine code by design for either of them (or any other 8-bit CPU from that era), and how this was achieved?

13
  • 37
    Do you consider FORTH high level? It generates very compact code that I'm guessing will compete with or beat C (on these processors) for speed. Commented May 29, 2020 at 14:26
  • 2
    @jon yes. Hence the question. Assembly takes much longer to write Commented May 29, 2020 at 15:09
  • 18
    As I understand it, AVRGCC produces quite decent code. Before arguing that C produces inefficient code on 8 bit architectures, one should (a) clarify whether it's a problem of 8 bit archictectures in general, or specific 8 bit architectures like the Z80 or 6502; (b) clarify whether this is an issue of the language, or something compiler specific (modern cross compilers can throw a lot more resources at optimization, plus 20-odd years of compiler development); (c) check if you're forcing the compiler to be inefficient by, say, using int where a uint8_t would suffice. Commented May 29, 2020 at 15:52
  • 13
    @MichaelGraf AVR is a modern CPU architecture that was designed to support C and other high-level languages. It's in an entirely different league than the "classic" 8-bit CPU architecture we talk about here.
    – user722
    Commented May 29, 2020 at 20:47
  • 2
    @mawg the linked question points out that some constructs in C are hard to create in Z80 machine code at all, even less efficient code. Naturally the compiler matters but it is about what the hardware offers. Commented May 31, 2020 at 10:30

15 Answers 15

62

One language that was popular on early 8-bit micros, including those that used the 6502 CPU, was Forth. Forth is exceptionally good for this use case, and superior to a C compiler, because Forth can make more efficient use of the 6502's hardware stack. Forth lacks any sophisticated methods of dealing with parameters. Everything is passed through the Forth stack and procedures just deal with the stack for both their input and output. This means the language doesn't require much from the CPU in terms of addressing modes or spend any time doing sophisticated effective address calculations.

Additionally, Forth provides a somewhat different paradigm than C in that it requires a program to be built up from very primitive and efficient units known as "Words" in Forth. By combining the primitive words into ever more complex combinations, the program is built up in a way similar to Functional Programming languages. This ensures Forth is very simple (and fast) to compile, even on 8-bit machines, and that the results execute very efficiently, given that the lowest level Words were coded to be efficient on the CPU.

According to some 6502 Forth users, the typical overhead incurred by Forth programs vs. similar functionality in Assembly is about 25%. And various Forth compilers for 6502 have been implemented in as little as 1.5 KiB. This fact makes Forth likely the only language compiler you will find running from an 8-bit computer ROM cartridge. So, it is both the low overhead of the compiler AND the efficiency of the resulting code that made it a favorite of early microcomputer programmers seeking something more "productive" than Assembly language.

20
  • 3
    FORTH was available as compiler language? Commented May 29, 2020 at 15:09
  • 4
    @MartinRosenau Yes. Generates machine code and no interpreter or runtime is required.
    – Brian H
    Commented May 29, 2020 at 15:13
  • 10
    @moonwalker Forth worked quite well on the Z80 too. There's the Jupiter Ace that had a bulit-in Forth instead of BASIC. Commented May 29, 2020 at 16:03
  • 9
    I worked (professionally) with Forth in the mid-80s, and don't remember any compilers that compiled to machine code. I think it would actually be less efficient in terms of space to do this, as well as in terms of time if it was implemented as subroutine calls. The standard Forth implementation stored a word's definition as a series of addresses, and the execution engine would use a jump instruction to go to the next address. Not saying it didn't happen, but I don't see why anyone would.
    – kdgregory
    Commented May 30, 2020 at 11:54
  • 3
    @JeremyP: If a Z80 Forth system were to use the system stack and HL for the operand stack (top of stack is HL; everything else is on the stack), code to push a constant would be simply "PUSH HL / LOAD HL,xx". Four bytes, including the two-byte constant. "+" would be "POP DE / ADD HL,DE". Two bytes. Operations that need to be handled by called functions would be a bit trickier. If one wanted to do a subtract that way, it might need to "POP DE / POP BC / AND A / SBC HL,BC / PUSH DE / RET". Two extra instructions to manage the return address, but if most operations avoid fn calls...
    – supercat
    Commented May 30, 2020 at 18:51
31

C can be greatly improved as a language for the 6502 and Z80, as well as micros like the PIC and 8051, if one abandons the notion that implementations must provide for recursive subroutine calls, and adds qualifiers for things in zero page or pointers that are limited to accessing such things, and (for the Z80) adds qualifiers to identify objects that are known not to cross 256-byte boundaries.

Ironically, platforms like the PIC and 8051 which can't really support recursion at all and would thus seem unsuitable for C end up having better C compilers than those like the Z80 and 6502 which can barely support recursion, and thus generate code which is reentrant but inefficient instead of efficient non-reentrant code.

25
  • 13
    This is why Small C (and its many flavors) flourished in the 8-bit world.
    – Jim Nelson
    Commented May 29, 2020 at 20:05
  • 1
    @JimNelson: Support for recursion is one of the biggest obstacles to efficient code generation. BTW, another useful feature on the 6502 I've not seen in compilers would be an ability to use a "register" qualifier on arrays that would only be accessed via the subscripting operator rather than member-type pointers. Such arrays, if 256 items or less, could be stored more efficiently with all low bytes together and all high bytes together, than as low-high pairs.
    – supercat
    Commented May 29, 2020 at 20:18
  • 1
    So you think of a subset of C? Commented May 29, 2020 at 20:21
  • 3
    @supercat To my understanding the only language which really had a standard back then for microcomputers were Standard Pascal. I fully understand why e..g. Turbo Pascal chose to rework the string support. I am interested in any high level language which translated really well to machine code. Commented May 30, 2020 at 18:15
  • 2
    @supercat C strings are as far as I know not a language feature but a library facility. This is not important for this particular question. Commented May 30, 2020 at 18:30
26

I know that the Z80 and the 6502 are very different, but I was wondering if there are any languages on a higher level than assembly which can generate compact and efficient 8-bit machine code by design, and how this was achieved?

Well, a prime candidate would be Ada.

It was a specific design goal for Ada to produce good code for tiny and 'odd' microprocessors (*1). Two basic approaches enabled this:

  • the language itself was as non-assuming as possible, while at the same time
  • offering tools to specify certain workings as detailed as possible - where needed -
  • separating this to a great degree from generic code.

The high abstraction separates it from 'lower' languages like C or FORTH which are both built around certain assumptions about how a processor works and what functions it offers. In fact, C and Forth are great examples of two major pitfalls:

  • Expecting a certain low-level behaviour of a CPU and
  • ignoring high-level functions offered by a CPU

C for example is built on pointers and the assumption that everything has an address and is a series of bytes which can be iterated over (and may be structured further, but that can be ignored at will). CPUs with multiple address spaces or object storage or different understanding of data handling will inherently end up with less than desirable code.

The /370 is a great example here. While (register-based) pointers are an essential feature, the memory pointed to is handled as a block (or structure) with sub-blocks (fields) that can be manipulated with single instructions, not loops (*2). C-code forcing iteration onto a /370 can easy degrade (local) performance by a factor of 100 and more (*3).

Forth on the other hand is at its core built around the idea of a stack (or multiple stacks) and the ability for threaded code. Effective (stack) pointer handling and fast (and simple) moves to and from the stack are essential for performance. Both issues that 8-bit CPUs aren't inherently good at. The 6502 may have 128 pointers, but handling them is ugly. Indirect jumps, such as those needed for threaded code, are non-existent. Thus, fast implementations rely on self-modifying code. Then again, it is only marginally better on an 8080/Z80 as they have only one memory pointer (HL) and a backup (DE) which in turn is needed almost all the time.

Like C, Forth ignores higher-level function offerings, or has a hard time using them. Unlike C, it's a bit more open to changes in low-level behaviour.

Both languages are maybe higher than assemblers can operate on a more abstract level - if used carefully - but are not inherently abstract. They assume certain workings. If these are not basic machine instructions, performance will suffer.

A 'real' high-level language should not make such assumptions. Here, Pascal is a better candidate, as it assumes next to nothing. As a result, there are compilers for either line, 6502 and 8080/Z80, producing quite good code. I guess Turbo-Pascal for CP/M doesn't need any further introduction. On the 6502 side (Apple, Atari, Commodore) Kyan Pascal was considered a great way to work in high-level languages (*4).

Which brings us back to the original question, how to achieve good code performance on a wide range of machines:

  • Don't expose any low-level working to the programmer.
  • Have the compiler cover it.
  • Have the programmer define the intended result, not the way it is achieved.

Essentially the goals set for Ada :)


P.S.:

... on a higher level than assembly ...

Serious? That statement feels quite offending :)

Assembly can and often is already on a higher level than some other languages. Assembly is the essential prototype of an extensible language. Everything can be done and nothing is impossible.


*1 - Note the 'produce' clause, having the compiler run on such machines is a different story.

*2 - It's always helpful to keep in mind that the /370 may have spearheaded many modern concepts, but it was designed with a punch card in mind. A punch card is a record, maybe pointed to by a register, holding information (fields) at fixed offset with fixed length. The whole instruction set for character (byte) manipulation is built to fit. No need to loop over two fields to move, compare, translate, pack or even search within, the most basic instructions (MVC, CLC, TR, PACK, TRT) already take care to handle fields at once.

*3 - This was a huge problem when C first became requested by users and implemented. By now compilers have evolved, and more importantly, CPU designers have added quite some 'changes' to cover up for the inefficiency of C.

*4 - Its only fault was its late 'birth' - too late to make a major impact.

17
  • 2
    Thank you for being thorough. I'm sorry if you are offended by asking about a higher level than assembly - wikipedia says "In computer science, a high-level programming language is a programming language with strong abstraction from the details of the computer. ". If your assembly language code abstracts strongly from the details of the computer, is it still assembly or some higher level language implemented in assembly macros? Commented May 29, 2020 at 20:17
  • 2
    A major issue with Ada is that the runtime library for the full language is very heavy. Ada 83 supports concurrency in a very first-class way, as well as exceptions, tagged unions, dynamically sized arrays of anything (including tasks, and as members of tagged unions), and other cool stuff. All that stuff fit onto an x86 (I was part of the Alsys team that created the first validated Ada 83 compiler for x86, hosted and targeted for the IBM PC running DOS) - but it would be a tight squeeze on an 8 bit CPU and would require whole program compilation (to leave out features you didn't need).
    – davidbak
    Commented May 30, 2020 at 16:13
  • 2
    There are indeed a lot of features in Ada that work nice on small systems, embedded systems - rich primitive types (including ranges of integers) among them. Someday I'd like to hear more of your 8051+Ada experience!
    – davidbak
    Commented May 30, 2020 at 17:58
  • 2
    @davidbak And that's why, to me, it is close to a perfect language for 8 bit systems. The application wasn't anything remarkable. Just a controller for an operator panel with two membrane keyboards, several seven-segment displays, a bunch of LED and two serial. Soft and hardware was rather simple. The nice part was an ability to constant self-testing of all items with reporting of any (upcoming) failure as well as denying usage in case of issues detected. It was part of a medical device, so higher standards. That's also the reason why the dreaded 8051 was used. It was considered proven working
    – Raffzahn
    Commented May 30, 2020 at 18:16
  • 4
    One facet of C's byte fixation is that it doesn't allow bit addressing, while many MCUs and even some CPUs (Z80) offer ways to address individual bits (bsf, bcf, btst instructions). This means that an Ada programmer can declare an array of booleans or a Pascal programmer can use a set, while the C programmer has to use ANDs, ORs, bit masks and their complements, (easy to get wrong!) and the compiler has to reverse engineer the intent into a bit instruction. Commented Apr 8, 2021 at 14:54
18

"Forth" was the first name that jumped to my mind. Another is Action!, an Atari 8-bit-specific language. (Its manual can be found on the Internet Archive.)

Action! is a structured Algol-inspired language that borrowed constructs from other languages (in particular, it offered C-like pointers and arrays) and native types that mapped cleanly to the 6502's memory model. Techniques that usually required assembly were possible, such as positioning code/data and trapping interrupts. Action! kind of stood between a full-featured macro assembler and a high-level language like Pascal. (That it didn't have native floating-point support or recursion is a hint of how pared-down it really was. This page has a nice summary of its limitations.)

I couldn't find hard numbers showing it was more efficient or faster than the Atari C compilers of the time, but this article from Hi-Res magazine shows Action! finishing a Sieve benchmark in the approximate time of a Z-80 C program.

Action! also offered a surprisingly full-featured IDE before the acronym was coined: Integrated full-screen text editor, in-memory compilation (which made it quite fast), and a monitor for debugging. Action! showed me how good tools make a big difference in the edit-compile-debug loop.

1
  • 2
    Action! was great. I was in high school when it came out, and it was a pleasure to use, being cartridge based and it had a great editor and fast compiler.
    – mannaggia
    Commented May 8, 2023 at 14:27
14

Ada for cross compilation; though there WERE native Ada compilers (e.g. Janus Ada, with a Z80 (Ada-83) release here and reviewed in 1982 here) it was stretching the capabilities of a 64kbyte machine. Side note : the response to review was by Randy Brukardt; in 2020 he is still selling Janus Ada and actively contributing to the comp.lang.ada newsgroup!
So, Gnat (utilising gcc and soon LLVM) can run on any decent host, and optimise pretty well for small targets - nowadays, AVR or MSP430. Ada is in some ways easier to optimise than C.

But one other candidate worth mentioning for native compilation would be Modula-2. A much smaller simpler (and yes, more restricted) language, rather in the Pascal mode, but much more amenable to compile on a decent Z80 system. I had the FTL Modula-2 compiler running on a Z80 CP/M system.

I don't remember specific benchmarks on Z80, but on slightly larger 8086/8088 systems (where "small model" executables were 64K) the JPI Topspeed Modula-2 compiler of the 1980s was probably the most efficient compiler for 8086 of any language in the DOS era.

2
  • 1
    I ran turbo modula from Borland. A rather nice product but I did not look at the machine code. Commented May 30, 2020 at 16:14
  • 2
    @ThorbjørnRavnAndersen I believe that was bought by JPI and developed into Topspeed. Commented May 30, 2020 at 16:15
13

The main problem for high-level-languages on these platforms, and especially the 6502, is the small hardware stack. 256 bytes does not give one much room to work with for languages that intend to push large activation records on the stack.

As others have noted above, the solution is to remove recursion from your language definition, and in a more general sense, any "local" information.

Also worth mentioning, in the 1970s and early 80s when these machines were the bomb, the language all the cool people were working with were the many variations of ALGOL. Most mainframe systems had a "systems programming language" based to some degree on ALGOL layout, and Pascal once that became, effectively, the "new ALGOL". C did not become the universal solvent until the 16/32 bit machines had been in the market for some time.

So for instance, on the Atari you had Action!, an ALGOL-derived language with no recursion. This not only reduced the size of the stack use, but also greatly reduced the complexity of a proc call, you basically just did the branch. This later bit remains a topic of discussion to this day, as in Swift where Apple tries to convince you to use struct instead of class to reduce call overhead.

Raff mentioned Forth, which was designed as a multi-platform language that used its own stack structure to provide C-like capabilities on machines that lacked the requisite hardware support. While I guess it was a success in that respect, I recall trying to program in it and having feelings much the same as drinking way too much cheap gin.

8
  • The hardware stack is not a limiting factor. Any modern C compiler is very good at keeping active variables in registers; and anyway, variables that cannot be stored in zero page, such as a C stack, will likely be stored in 16 bit memory.
    – johnwbyrd
    Commented Apr 9, 2021 at 7:04
  • 3
    Modern C compiler in 1980 on an 8-bit machine? Registers? We're talking about an 8-bit 6502 with a single ACC and an 8-bit SP. Commented Jul 27, 2022 at 16:01
  • @johnwbyrd: A well designed 6502 toolset should be able to keep automatic-duration objects in zero page, with routines that are never active simultaneously being able to share the same storage. If one can build a worst-case call tree, the amount of storage a simple algorithm would end up allocating would be the same as what would be needed on a stack if all function calls on the tree could actually be performed.
    – supercat
    Commented Oct 15 at 15:00
  • @supercat Naturally; see also llvm-mos.org for a practical example of this. In our implementation, we refer to zero page as "imaginary registers" and we let LLVM have at the problem of figuring out what goes into them at any given moment.
    – johnwbyrd
    Commented Oct 24 at 18:19
  • @supercat The issue of which allocator should manage said imaginary registers, is a topic of spirited discussion on our Discord. Some are in favor of LLVM's current greedyish allocator; some people want to try a PBQP allocator; I'd like to see them all implemented and benchmarked.
    – johnwbyrd
    Commented Oct 24 at 18:30
12

Despite the other answers posted here, Forth generally performs significantly worse on the 6502 than an optimizing C cross-compiler like CC65. In tests I did comparing it to Tali Forth 2 for the 65C02 [1], which generates the fastest type of Forth code called STC, Forth code is sometimes on par with the C equivalent but more often 5-10 times slower. As far as I can tell, these are the main reasons:

  1. All values pushed on the stack in Forth become 16 bit, which takes the 6502 a lot longer to manipulate than 8-bit values. C, on the other hand, has 8-bit types which are much faster to work with.

  2. Forth words constantly adjust the data stack as they push and pop things, while C functions tend to do most of the stack allocation at the beginning and end of a function, which is much more efficient.

  3. 6502 Forths don't generally do any optimization, even when enough information exists at compile time to do so. Something like "drop 5" in Forth will increase the stack pointer to do the drop then immediately decrease it to push the 5, so you get the useless series INX / INX / DEX / DEX. CC65 optimizes this type of inefficiency out in some but not all cases.

  4. 6502 Forths also don't optimize for constants. CC65 outputs more efficient assembly for something like "foo<<3;" than "foo<<bar;" since the number of shifts is known at compile time. Forth generates the same code in both cases, always using the most compatible but slowest version.

  5. Constraining the programmer to only modifying the top levels of the stack produces less efficient code. For example, you can't step over the first item on the stack and add something to the second. The equivalent "swap 5 + swap" wastes time on the two swap operations to get the value to the top of the stack and back into second place, while C can just directly modify any item on the stack.

CC65 is not perfect, but you're unlikely to get anything near as fast as that without writing the assembly yourself.

[1] http://calc6502.com/RobotGame/summary.html

6
  • 1
    Do you have a link to the programs you compared?
    – UncleBod
    Commented Jun 20, 2020 at 17:20
  • @UncleBod, I've been working on a web page to show the tests and my data. I'll try to post it here in the next day or two. Commented Jun 20, 2020 at 17:22
  • 1
    @UncleBod, link to the test I did: calc6502.com/RobotGame/summary.html Commented Jun 30, 2020 at 12:55
  • If one didn't need to go beyond 256 words of stack, I would think a 6502 forth-like language could be reasonably efficient if it e.g. stored the low bytes of all 256 stack entries at $9E00-$9EFF and the high bytes at $9F00-0x9FFF. Something like "add" could be "lda $9E00,x / clc / adc $9E01,x / sta $9E01,x / lda $9F00,x / adc $9F01,x / sta $9F01,x / inx".
    – supercat
    Commented Apr 8, 2021 at 15:49
  • @supercat, you can get twice as much stack space like that, but each of the 6 memory accesses you have for "add" will be 1 cycle slower than putting a smaller stack in zero page. Also, you can't use the values on your stack as pointers since they have to be in zero page and have to be stored contiguously. Storing them in higher memory also doesn't solve any of the speed problems above. On the other hand, 128 or less words of stack in zero page is way more than you'll probably ever need in Forth. Commented Apr 9, 2021 at 18:04
8

For 8080, 8085 and Z80, possibly PL/M. That generated exactly what you told it. Also, it had special I/O instructions. With most other compilers, you had to call

output(0x20, 0x90)

but in PL/M it was built in

output(0x20) = 0x90

would generate the out instruction. There was a similar input instruction. The part of PL/M that always caught C programmers out was that even numbers were false and odd numbers were true. This gave rise to PL/M86 and PL/M286.

The use of Forth varies

  1. as a compiled language
  2. as a concept with a generic interpreter
  3. as a concept using indirect threaded code (https://en.wikipedia.org/wiki/Threaded_code#Indirect_threading) with a home-brew interpreter
  4. as a concept using knotted code (also known as token threads) with a home-brew interpreter.

I've seen 3 and 4 but not 1 or 2. Option 3 and 4 is normally used to reduce the code size but the program runs more slowly than if it were written in straight code. In the late 70s and early 80s, when information was obtained from journals, it wasn't easy to find a Forth compiler so most of the time it was a home brew version and everything was written in assembler.

Edit Token threads: Token Threads

I've looked through my old notes on Forth but I can't find the reference that describes the use of token threads in Forth so I'll describe it here. This is all about space saving on space limited machines.

Direct threading: In normal assembler, we have something like

main:  call func1
       call func2
       call func1
       call func3
       call func1
       call func2
       call func3

8/16-bits: 21 bytes
32-bits: 35 bytes

Just adding 32-bit for completeness. This does not apply to Z80s.

Indirect Threading: For a 16 bit machine, there are 3 bytes for every call. For a 32-bit machine, there are 5 bytes for every call. In most Forth implementations, there is a small piece of code (the exec) that reads the address and executes, a bit like an indirect function call in C/Fortran. This saves 1 byte per call, which is significant if the saving exceeds the size of the exec code

main: word func1
      word func2
      word func1
      word func3
      word func1
      word func2
      word func3

8/16-bits: 14 bytes
32-bits: 28 bytes

Token threading: If there is a large number of these, and there aren't more than 256 functions, it is possible to save more space by using a lookup table

lut:  word func1
      word func2
      word func3
main: byte 1
      byte 2
      byte 1
      byte 3
      byte 1
      byte 2
      byte 3

8/16-bits: 13 bytes
32-bits: 19 bytes

This is a further "saving" of 1 byte for every word in 16-bits and 3 bytes for every word in 32-bits: that is provided the "saving" exceeds the size of the exec.

One byte may not seem significant but in a tightly packed PROM, sometimes it makes the difference between having an 8K ROM or redesigning to accommodate a 16K ROM.

6
  • Do you have a pointer to information about "knotted code" or "token threads"? web search (for me) turns up only this answer for either of those terms ...
    – davidbak
    Commented Aug 22, 2022 at 20:26
  • 1
    I'll have a look through my photocopies of my old Forth docs see if I can find the actual reference. It will take a while - it is up in the attic and hasn't seen daylight for about 30 years
    – cup
    Commented Aug 23, 2022 at 7:06
  • Notably, PL/M functions are not required to be re-entrant so their local variables get stored in memory rather than on the stack. However, there is still room for improvement; at least with the compiler used to build CP/M, the code generated seems to include a number of unnecessary loads of values that are already in registers.
    – john_e
    Commented Aug 28, 2022 at 9:35
  • Very often, the people who do the code generation, don't really know the underlying assembler. The compiler emits an intermediate language which is interpreted into the underlying language. This can lead to unnecessary loads. Many of these "home brew" intermediate languages (at least, the ones I have used) don't have the concept of registers.
    – cup
    Commented Aug 28, 2022 at 10:00
  • 1
    For PLM, the optimizations are listed in chapter 12 of the manual bitsavers.trailing-edge.com/pdf/intel/PLM/…
    – cup
    Commented Aug 29, 2022 at 7:08
6

It only has to do with the effort put into the code generator back-end. C is an abstract language, it doesn't need to directly reflect what the machine is doing. But this is the sort of stuff that would be state-of-the-art in 2020, and would require significant investment. There's nothing inherently special about Z80 of 6502 in this respect - only that the impedance mismatch between some platforms and the code generator back-ends is very high. For Z80 and 6502 it wouldn't matter what the language is, because the specifics of the language are far away and dissolved by the time the intermediate representation gets to the optimizer and code generator. Any high-level compiled language would be just as bad on Z80 and 6502 as C is, pretty much.

We're spoiled with excellent modern compiler back-ends. The trouble is that they are commonplace that everyone thinks it's "easy" work. Not at all. They represent man-decades of effort if someone were just to reproduce them.

So, you can get excellent Z80 and 6502 code out of a C compiler if you hire a couple LLVM back-end experts out from Apple and Google, pay them the going rate, and let them at it for a couple of years. A couple million dollars is all it'd take, and you'd grace the world with absolutely amazing Z80 and 6502 code produced from both C and C++.

So: I'm sure the results would be excellent- but it requires lots of effort. It's the sort of effort that historically has not been expended by even major silicon vendors, with exception of Intel, Digital and IBM. Zilog's own compilers (all of them, doesn't matter what year was the release) are junk when you compare what they manage to cough up to x86 or ARM output from C code passed through Clang and LLVM, and all the man effort put up by, say, Zilog and Motorola compiler teams throughout the 70s, 80s and 90s, all together in total, was completely eclipsed by the man-hours that went into, say, Clang+LLVM in the first decade of the existence of both projects. Zilog's and Motorola's marketshare back when they still had plenty of it absolutely didn't improve matters here: they were a bit too early and the everyday techniques used by e.g. LLVM weren't available and/or they required so much memory and CPU cycles to run that it wasn't feasible to offer such products to wider audience, because you pretty much needed a heavy minicomputer or a top-notch workstation to do this sort of work.

11
  • 3
    Do you have actual experience with 8-bit cpus? Commented Jun 28, 2020 at 10:35
  • 2
    Semi-regrettably so. I'd say half of the code I wrote over the last 3 decades of me writing code was for 8-bit CPUs. When I bash Zilog and Motorola compilers, it's based on experience all too intimate :/ But in short: If humans can write acceptable Z80 assembly, so can a C compiler that got sufficient amount of money and talent dumped into it, under leadership of a suitable luminary. That's "all" there's to it, at the end of the day: how many $$$ were spent (or spent not) on that problem - and were they spent productively. Commented Jun 29, 2020 at 2:14
  • 3
    Getting good C or Pascal-language performance requires foregoing recursion in all cases where it isn't needed, and using a linker that can statically overlay automatic objects ("local variables") used in different functions. I don't think LLVM is really set up to accommodate such things. Further, modern compiler back-end optimizers seem to have baked-in assumptions about the relative costs of various actions, and generate sub-optimal code for platforms where the actual relative costs of actions don't fit those assumptions.
    – supercat
    Commented Jun 29, 2020 at 16:46
  • @ReinstateMonica: An annoyance I have with the design of C is the lack of byte-based pointer arithmetic operators. When using arrays of 16-bit values in machine code, especially on the 16-bit 8088/8086, or even more so on the 68000, it often makes sense to have indices step by two to exploit the [bx+si] or [si+const] or @(Ai+Dj.w) addressing modes, but there's no nice way to express such concepts in C.
    – supercat
    Commented Jun 29, 2020 at 17:21
  • 6
    "We're spoiled with excellent modern compiler back-ends. The trouble is that they are commonplace that everyone thinks it's "easy" work." This seems to be almost impossible to explain to people who keep saying that this or that optimization just should be there, no big deal. I really appreciate you saying this so explicitly.
    – introspec
    Commented Jun 29, 2020 at 20:24
5

I suggest PLASMA (https://github.com/dschmenk/PLASMA), a C-like language that compiles to interpreted code. It has a much higher code-density than assembly language, and it's much faster than FORTH.

2
  • 1
    That's the third PLASMA programming language I heard of. The most well-known is probably Carl Hewitt's PLASMA language he designed to illustrate his newly-invented Actor Model of computation, and a member of the MIT AI family of languages (LISP, PLANNER, Microplanner, PLASMA, Scheme). The second one I forgot. And just now I found this: plasmalang.org . Commented Jun 1, 2020 at 16:20
  • @wilson It could compete favourably with even compiled Forth, based on the examples of compiled Forth that I have seen, but maybe I've been looking at the results of a poor compiler. Commented Jun 5, 2020 at 22:51
4

I know that the Z80 and the 6502 are very different, but I was wondering if there are >any languages on a higher level than assembly which can generate compact and efficient >8-bit machine code by design for either of them (or any other 8-bit CPU from that era), >and how this was achieved?

I've been working on my own high-level language "Higgs" which targets 6502,65C02,68000,68040, RISC DSP and recently started working on Z80 backend.

The output (build script called from within Notepad++) is an assembler file that is then fed into the local assembler/linker of the respective platform.

The feature list of the language depends directly on the target platform's abilities. Each HW target has different set of unique features, dictated by the addressing modes / asm capabilities of the platform. Arrays on 6502 are very different than arrays on 68000 or DSP RISC.

Each target however supports global/local/register variables, global/local constants, structures, arrays, functions (with optional parameters), loops, conditions, nested blocks (helps with formatting and namespace pollution), 3-parameter math expressions, signed math (if present), increment/decrement (var++, var--).

My basic rule is that I never include a new feature unless I can guarantee that the code generated by my compiler is identical to the code I would write manually, directly in ASM.

From experience of writing my own game in it (~25,000 lines of Higgs so far), it's exponentially faster to write/debug/test new code compared to ASM. Less than 0.01% of code is still written in ASM, the rest is Higgs.

I will be adding Z80/Next backend soon.

If you could only have 3 features that would increase your productivity, this is what gives you most return:

  1. conditions
  2. math expressions
  3. scope-based variables/constants {}

Here's an example (68000 target: hence d0-d7/a0-a7 registers, .b, .w, .l sizing, etc.), showing how high-level it is (compared to ASM) and that it really feels almost like C, and is thus very easy to come back to, after 6 months, and quickly understand and adjust the code (unlike hand-written ASM that mostly evokes deep WTF feelings):

Render_LaserShots:
{
    local long lpMain

    {   ; Player LS
        colorQuad = #$FFA080
        SLaserShot.InitRegister (LaserShots)
        loop (lpMain = #MaxLaserShots)
        {
            if.l (SLaserShot.IsActive == #1)
            {
                d1 = #0 - SLaserShot.X
                d2 = SLaserShot.camY
                d3 = #0 - SLaserShot.camZ
                SetCamPos32 (d1,d2,d3)
                Render_obj3DList_Object (LaserShotMeshPtr,#PolyCount_LaserShot)
            }
            SLaserShot.Next ()
        }
    }
    {   ; ShootingEnemy  LS
        SEnemy.InitRegister (MainEnemy)
        if.l (SEnemy.State == #AI_STRAFE)
        {   ; Only Render Enemy's LS if he is active
            colorQuad = #$40FF40
            SLaserShot.InitRegister (EnemyLaserShots)
            loop (lpMain = #MaxLaserShots)
            {
                if.l (SLaserShot.IsActive == #1)
                {
                    d1 = #0 - SLaserShot.X
                    d2 = SLaserShot.camY
                    d3 = #0 - SLaserShot.camZ
                ;   print3 (d1,d2,d3,#50,#20)
                    SetCamPos32 (d1,d2,d3)
                    Render_obj3DList_Object (LaserShotMeshPtr, #PolyCount_LaserShot)
                }
                SLaserShot.Next ()
            }
        }
    }
rts
}
4
  • It will be very interesting to see what you end up with languagewise. Commented Jun 21, 2020 at 14:00
  • 1
    Note that a good optimizing compiler can do tricks in assembly that may not be what you would write by hand. Commented Jun 21, 2020 at 14:01
  • 1
    @ThorbjørnRavnAndersen : True - once the code is done, there are so many things you can do, to butcher the code, make it faster, remove CLC/SEC here and there, or notice you could (ab)use your index registers in a way that will make them directly reusable in next stage. But, that creates a totally unmaintainable code. Now, I've done that in past,but for me, it's of highest importance to be able to come back later and adjust the code. Which is impossible, once you butcher it manually. Since the code is high-level, I have zero qualms about discarding it altogether (unlike hand-butchered code) :)
    – 3D Coder
    Commented Jun 29, 2020 at 7:56
  • @ThorbjørnRavnAndersen: Short-term, my ToDo list shows in Top 3: function return values and classes. Those two seem to promise to bring most productivity. After that, I would probably focus on multi-platform codebase - e.g. writing game code once, but reusable on multiple HW targets.
    – 3D Coder
    Commented Jun 29, 2020 at 8:05
4

This is my experience with C on Z80 and 6502:

  • Zilog Z80/z88dk

    The code generated is pretty decent, not as good as handwritten assembly but good enough for lots of purposes. One advantage on Z80 in relation to C is the existence of IX/IY registers that are used for local variable access / parameter passing. Of course they aren't as efficient as register parameters but it still works good with C paradigm. I tested switching to static variables and found there was a difference, but small.

  • 6502/cc65

    I'm not familiar with 6502 assembly too much but aware of general architecture. Whether I compiled the code with or without static variables for 6502 the impact was very big (IIRC up to 50%). I can't compare with handwritten code since I have no experience.

Bottom line: there is a big difference in processor architecture. Zilog Z80 is much more C friendly, it has decent stack, index registers that allow quite straightforward implementation of many C paradigms, calling conventions, etc. While 6502 is much more limited in implementing re-enterable code or using stack-based variables.

1
  • 3
    My experience is that on the Z80, there is a huge cost difference between the cost of entering a function that uses no parameters or automatic objects, versus calling one that uses some. Once a function is entered, the cost of accessing 8-bit automatic objects using IX addressing isn't too totally outrageous, but accessing 16-bit values using IX often costs at least twice as much and sometimes more. By contrast, 16-bit instructions like mov hl,(addr) or mov de,(addr) are less than 25% more expensive than the 8-bit mov a,(addr).
    – supercat
    Commented Jun 21, 2020 at 21:57
3

In terms of performance, assembler is obviously the most suitable language for a specific, low-end core. Period. And this is what everyone used up until the late 1990s/early 2000s. Then people started to notice that it truly and utterly sucks to have a big assembler code base for one MCU and port it to another. That's usually even more work than rewriting the program from scratch.

And so C for microcontroller systems started to gain momentum. Not because of performance - the main reason was portability/maintainability. But it was also easier to find C programmers than to find "programmer of obscure assembly language x".

So at the time when C was becoming popular for these kind of systems, parts like Z80 were already obsolete since forever. There was probably not much commercial interest in porting compilers to already outdated parts like Z80 or 6502. Meaning that the C compiler ports you would get were of diverse quality. There are loads of godawful embedded C compilers out there for sure. Hobbyist ports that are ironically of better quality might have emerged since then.

However, the reason why some code performs poorly on Z80 has very little to do with the choice of programming language. Those who think otherwise are naive, there are very few things in assembler which can't be done in C, such as directly manipulating the stack pointer (which is a good thing most of the time). There's always a choice in C of how many abstraction layers, if any, you wish to craft. If you stick to raw low-level code, there's no reason why it would perform worse.

But like assembler, C has to be hand-crafted to suit the specific target. It is tricky to write C for 8-bitters, far more so than for 32-bitters. I wrote a few basic things to keep in mind here. There's a whole lot of other details too, things that come with experience and C skill. You can't just take any PC programmer and tell them to write C code for a Z80 - that ain't gonna end well.

Some arguments from the linked question are just nonsense. For example there's no such thing as "C programs use the stack more extensively than assembler". They only do if the programmer lets them. I've written C code for extreme low-end parts that didn't even have a stack! No problem if you know what you are doing. If you don't know what you are doing, the choice of programming language won't matter.

12
  • It's unfortunate that the Standard didn't expressly treat support for recursion in freestanding implementations as a desirable, but not mandatory, feature (nothing requires that implementations support function nesting more than two deep, or would forbid an implementation from processing recursive calls made by any function other than main nonsensically, but doing so would have been seen as an abusive interpretation of the Standard). Compilers for really primitive 8-bit CPUs which really can't handle recursion support automatic-duration objects much better than Z80 compilers do, but...
    – supercat
    Commented Oct 11 at 17:08
  • ...a Z80 compiler that wasn't designed to support recursion could support such constructs much more efficiently.
    – supercat
    Commented Oct 11 at 17:09
  • … and that’s exactly why I asked the question… Commented Oct 11 at 17:11
  • But in the late 1990s and early 2000s, it was the introduction of new hardware architectures that were designed to fit better to run C that then made it possible to introduce better C compilers for these architectures. Such as AVR.
    – Justme
    Commented Oct 11 at 18:42
  • Anyone using recursion in firmware for extremely limited 8-bitters truly deserve all problems coming for them... As mentioned in the answer: "You can't just take any PC programmer and tell them to write C code for a Z80 - that ain't gonna end well."
    – Lundin
    Commented Oct 14 at 6:50
1

While this assumption was true for decades, today, the assumption in the question is now incorrect. The common wisdom about C being a bad choice for the 6502, is proven wrong by counterexample.

The choice of source language, does not gate the quality of generated code for the 6502. In fact, code quality on the 6502 (or any other platform for that matter) is gated by the quality of lowering intermediate-representation code to MOS instructions.

Any 6502 codegen must take into account the 6502's architecture quirks: there are few real registers, but there's also a bunch of zero page memory. There are many ways to solve this problem; we treat zero page as a set of imaginary registers, and use LLVM's register allocator to manage zero page with high efficiency. Additionally, we implement an emulated stack which behaves as if a real C stack were present on the 6502, without the inherent performance limitations.

EDIT: Not sure why this answer, which is entirely correct, is being downvoted. Here is an example of llvm-mos being used to port Linux to a virtual machine running on a Commodore 64. Here is an example of a new implementation of CP/M for many 6502 targets, again brought up using llvm-mos. Here is an example of llvm-mos optimizing a simple function to occur entirely at compile time. And here is an llvm-mos benchmark optimizing out the entirety of dhrystone, to compute its result entirely at compile time.

9
  • 2
    What non-toy or non-hobbyist compiler do you know of doing this? Commented Apr 9, 2021 at 20:31
  • 1
    llvm-mos.org
    – johnwbyrd
    Commented Jul 28, 2022 at 20:48
  • @johnwbyrd: There are many situations where an optimizing transform would likely be unlikely to adversely affect the behavior of a C program, but cannot be proven sound under the rules of the language. An optimizer that performs such transforms may be able to yield better benchmark results than one which refrains from making optimizations that cannot be proven sound, but such optimizers should nonetheless be recognized as toys unless they document all the cases where optimizations may yield results inconsistent with the language spec--something that so far as I can tell LLVM has yet to do.
    – supercat
    Commented Jul 28, 2022 at 21:56
  • 1
    @PatrickSchlüter: Depending upon the code generation strategy, one may not need an optimizer. In particular, if one has separate "generate machine code to evaluate an expression whose MSB will be ignored" versus "generate machine code to evaluate an expression whose MSB will be used", then given someByte = x+y; a compiler can call the "compute plus while ignoring MSB" routine which can in turn invoke code to compute the operands while ignoring their MSBs. If the evaluate routine has a sub-variant to "indicate whether expression can be computed very cheaply", then code can...
    – supercat
    Commented Oct 15 at 14:56
  • 1
    @PatrickSchlüter llvm-mos has approximately 100 passes or so, approximately a dozen of which are specific to the 6502 series.
    – johnwbyrd
    Commented Oct 24 at 18:38
0

Funny that nobody mentioned the high-level languages people really used on 6502 and Z80 machines at that time: Pascal. We used Pascal at that time as it was quite decent performance wise and much more "professional" than BASIC.

On the Apple II, UCSD-Pascal even with its P-Code interpreter managed to generate relatively good performance. It could obviously not be compared with pure assembly, but it significantly faster than BASIC and it allowed to build quite sophisticated applications thanks to its module (units) system, assembly routines and quite the collection of libs. Even some games had been written in UCSD-Pascal (the Wizardry series of RPG).

On CP/M it was Turbo Pascal that revolutionized compiling and writing apps.

10
  • Do you have insights and/or benchmarks in the code generated and executed? Commented Oct 14 at 11:57
  • I fail to see how compiled pascal code could become better than equivalent compiled C code. From the code generation viewpoint, pascal and C are pretty much the same.
    – lvd
    Commented Oct 15 at 5:14
  • 1
    @lvd: The C preprocessor would slow down compilation times, but compound assignments might have been able to improve execution times when targeting something like the UCSD P-system (especially if the P-code language included compound assignment operations). I think the main point of this answer was to observe that Pascal was actually used on 8-bit systems.
    – supercat
    Commented Oct 15 at 14:44
  • @lvd: In the 1980s, compilation times were a bigger deal than execution times (hence the popularity of BASIC with zero compilation time but slow execution), and slowing Turbo Pascal's build performance by an order of magnitude to accommodate the C preprocessor would not have been a popular concept. A Turbo-Pascal-derived language that added a few of C's operators could have offered the best of both worlds.
    – supercat
    Commented Oct 15 at 14:48
  • @lvd - as far as I understand it, the issue is pointer arithmetic, which does not exist in Pascal. Pointer arithmetic is why all that 'restricted pointer' palaver exists in ANSI C.
    – dave
    Commented Oct 15 at 19:44

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .