PDP-8 Small-C Compiler (2012)
so-much-stuff.com
so-much-stuff.com
To give an idea of why this is so hard, an PDP-8 without any add-ons has just two registers: a program counter and an accumulator. Similar to the MOS 6502, you instead used the first part of memory, the 'zero page,' as a register file. Self-modifying code was used to do subroutines, as was rather common at the time. You'd load the return address into the accumulator, call the subroutine, and then the subroutine would store the return address for later inside of its own code. A stock PDP-8 also had only 4096 12-bit words to work with at most.
The PDP-8 was envisioned more as a piece of lab equipment, not as a general-purpose computer. That an operating system could run on it astonished its creators.
Then we have the EE implementation. Digital came up with all the tricks in the book to save components: wired OR gates, gates that act as NAND or NOR depending on the input voltage, diode-capacitor gates and more.
The "straight" 8 is an EE marvel. Without any extension such as floating point arithmetic, they managed to engineer its logic in DTL using an outstandingly low count of 1409 transistors. I don't think anything has ever come close to that. In comparison, a 6502 requires around 3500, more than twice the straight 8.
And the PDP-8 could use multiple disk storage and teletypes at the same time, drive lab equipment, run an OS, have several users... it was quite more versatile and useful than 6502 8-bit computers.
This makes the PDP-8 one of the more interesting computer architecture to study, and indeed it has been used as a reference in many EE books. In particular, check out "The Art of Digital Design", Prosser, 2nd edition, which re-creates one in TTL logic.
To quibble a bit about the 6502, it was a bit more complex than the -8, but not by nearly as much as you say. The 6502 had 4528 transistors, including diode-connected transistors and depletion-load transistors used as pullup resistors. In its NMOS logic, an N-input NAND gate required N enhancement-mode transistors and a depletion-load pullup; in the straight 8's DTL, an N-input NAND required one transistor, a pullup resistor, and N diodes. So 1409 transistors is more or less 1200 (inverting) logic gates, and 4528 transistors is probably closer to 2000 gates.
The Intersil 6100 was a CMOS PDP-8, with 2N transistors per N-input NAND. It had 4000 transistors, which is probably also about 1200 inverting logic gates, but it included the EAE.
Further comparison at https://retrocomputing.stackexchange.com/questions/5843/pdp-....
To quibble a bit about subroutine calls, JMS didn't store the current PC at memory location 0; it stored it at the operand address, then continued execution at the next memory word after that. So the called subroutine had its return address just before the beginning of its code. This mechanism doesn't directly support recursion but at least it does directly support more than one level of subroutine calls, which your alternative wouldn't.
...
JMS FOO
... <- return here
FOO, 0
... <- first instruction executed
...
JMP I FOOThe degree of transistor parsimony is astounding; you could sort of do position-independent code by making it "PC-relative", but that replaced the low 7 bits of the PC with the operand field rather than adding them to it. Probably more ergonomic in a way, but it also saved a 12-bit adder! The Intersil 6100 implemented a full PDP-8 with the optional EAE in only 4000 transistors, less than a 6502; this is particularly impressive when you consider that the 6100 was CMOS, which uses significantly more transistors than the 6502's NMOS.
One of the most interesting things I've seen come out of the PDP-8 world in recent years is John Cowan's PDP-8/X, a 32-bit PDP-8: https://github.com/johnwcowan/pdp8x
The Data General Nova was strongly inspired by the PDP-8, but it had four accumulators instead of one, and you could index off two of them as well as the PC and the zero page. The Xerox Alto was a Nova clone with reprogrammable microcode, and the early Smalltalk virtual machines were implemented in its microcode. In an analogous fashion, Smalltalk-80 allows fetches and stores to index off of any of four base registers: the current method object (for constants; fetch only), global variables (indirected through the current method object IIRC), the stack frame, and the current object, with the offset from the base being a field in the instruction byte in most cases. And, as is well known, instead of an accumulator it has an operand stack (but, for example, no swap instruction). Smalltalk-80 has the densest code I've ever seen in any system.
I was thinking it might be interesting to do something like that in a closer-to-the-hardware PDP-8-like fashion, avoiding the need for self-modifying code and making subroutines reentrant by default. PC-relative loads would replace the low N bits of the instruction, as on the -8. Global loads and stores would use the zero page, as on the -8. Stack-frame loads and stores would replace the low 4 bits of the stack pointer, which would be incremented and decremented by 16 on call and return, thus also needing no adder. And a fourth base register, the "workspace pointer", would be XORed with the N bits from the operand field.
Objects to be pointed to by the workspace pointer might be variable in size; you might carve one page up into 4-word objects, while another one was divided into 64-word objects. When you were messing with one of the 4-word objects, you just wouldn't use "offsets" of more than 3 in your workspace-pointer-relative instructions, because those would index into some other (possibly unrelated) object of the same size. Four XOR gates instead of a full carry chain.
I feel like such a machine with a load-store architecture and an operand stack instead of an accumulator could be extremely ergonomic, much more than the -8, in very little logic.
http://www.merlintec.com/download/jecel_phd_deposited.pdf
The basic idea is that each instruction mentions two operand registers and one destination register. There are 4 kinds for the first operand and the destination:
t0 - t31: actually map to the special stack cache. Changing the frame pointer will remap them to a different area, possibly causing a line to be reloaded.
i0 - i31: actually map to the data cache as the first words of the receiver. Changing the self pointer will remap them to a different area.
s0 - s31: control registers for DMA-like logic that allows values to be streamed to other processors or to memory
x0 - x31: a mix of registers that control the hardware, including the frame pointer and the self pointer. Also allow pushes and pops to the stack.
The second operand can be these as well, but can also be several kinds of constants and can (L0 - L31) fetch the literal values in the current method, which also allows indirect access to globals.
The stack cache operates on linked lists of 32 word blocks (the current frame can span two such blocks) so the hardware to map tN to a cache word only needs a five bit adder
t0-t31 and i0-i31 do sound very similar, although off the cuff it sounds like you're designing for a processor about two or three orders of magnitude larger, like 400,000 to 4 million transistors rather than 4000.
The reason I was thinking XORs instead of adders is that if you have, say, 16-bit addresses computed from a 16-bit base pointer and a 5-bit offset, it's true that you only need five full adders, but you still need 11 more bits of carry chain (or 10 bits if your 16-bit addresses address bytes and your offset units are 16-bit words, as is usual in Smalltalk-80).
https://prezi.com/yhw-a3i0rnri/adaptive-compilation-for-an-o...
The focus for SiliconSqueak 3 was to have a small die area for the version without the ALU Matrix coprocessor. The inclusion of a total of 20KB of caches does bump up the transistor count by 20*1024*8*6 = 983,040 transistors all by itself though these tend to be far more dense than logic transistors. The rest of the processor is reasonably small.
The stack cache is organized as 32 word blocks that are aligned in memory on 32 word boundaries. At any instant pseudo registers t0 to t31 are covered by two blocks, named low and high. Adding the register number to the bottom 5 bits of the frame pointer gives you a five bit result plus carry. The carry selects between the low and high blocks while the five bit result is the word within that block.
When the frame pointer moves out of the low block, new low and high blocks are selected by following a linked list. This allows stack blocks from multiple threads to live side by side in the cache.
1. What I'm proposing for operands is to have a limited-depth hardware operand stack on the chip (3-10 items) instead of an accumulator register. Maybe a bidirectional shift register with two taps to draw off the top two stack items every N bits, for example. The B5500 kept its "hardware" stack in RAM.
2. Separate operand stack and call stack, not unified like the B5500. The call stack in memory doesn't support pushing and popping values, just indexing off it for local variables; in essence you have a call stack frame base pointer register but no call stack pointer register.
3. It's a load-store architecture, which is the opposite of super-CISCy and in particular the opposite of the B5500. No type tags! Hopefully no microcode! Like the PDP-8, you can put your large constants in a constant pool. (Maybe allocate a couple of ALU instructions for pushing 0, 1, and -1.)
4. Fixed-size instructions, like the -8. Probably more than 8 bits per instruction (weren't B5500 syllables 12 bits though?) so that you have room for an offset field of at least 6 bits.
Finally, a quibble: as I understand it, it's not so much that dense code is important when memory access is very expensive as that dense code is important when memory is expensive. RV32C has better code density than the B5500* and typically gets a significant speed boost out of it because that significantly boosts I$ hit rates for any given cache size, and I$ is always expensive. But think about small CPUs in general: if your entire CPU is 4000 transistors, an extra 1024 bits of 6T SRAM would cost 6144 more transistors, which drops the number of CPUs you can get onto a die by more than half.
______
* Even though 16-bit RV32C instructions are bigger than 12-bit syllables, they have source and destination fields, so you need less of them.
http://relaysbc.sourceforge.net/
But how do any PDP-8 compilers deal with the paged memory system? I mean one luxury of 6502 is PC-relative branches. On the PDP-8, jumps reach only within the current page.
PC relative is nicer because it has this property: shortening a branch is guaranteed not to make other branches longer. So you can optimize branches using a relaxing algorithm: you repeatedly try to shorten every branch in the code. If any were shortened, you repeat the whole process since prior shortenings may have made other branches shortenable.
On the PDP-8 this rule does not hold, shortening a branch can cause other branches to become longer, because the different code position may push the target into a different page.
PIC microcontrollers also have this problem.. but on the PDP-8 the problem is worse in that the page sizes are smaller.
Hmm, well here is one algorithm: in one phase shorten branches, but do not change the code size (insert NOPs to make the long branch the same size as the short branch). So this will increase performance, but not reduce code size. In second phase try to delete the NOPs. Only delete a NOP if it will not break any branch. This is not optimal, but maybe good enough..
Ugh, it's even worse because long branches on the PDP-8 involve indirect addressing, where the long address has to fit in the current page. Wow even PIC is nicer..
Anyway for Small-C, they talk about these issues here: