Close

Emit Is Commit

A project log for PDP - Processor Design Principles

Distilling my experience and wisdom about the architecture, organisation and design choices of my CPUs

For all its failures, F-CPU FC0 used a principle borrowed from common sense and the CDC6600 : Out Of Order Completion. Along with the weird load/store instructions that then became the fully architecture-visible address registers, this shields the main blocks of the CPU (decode, execution and memory) from each other.

It has been decided that a decoded/emitted instruction can not be rewound back as if it didn't exist. This would create a nightmare of register renaming, issuing and retiring that might bring 20% of performance but increase the size by 100% at least, and testing/validation become worse.

Then there is the SPECTRE class of attacks that play on prefetchers, speculative execution and other dynamic states.

Furthermore, typical OOO cores require branch prediction because the pipeline depth is seriously increased. For the same size you get two in-order-emit cores and twice the branch decision throughput without the hassles of a huge branch history table, you can even multithread/multiplex the threads.

All in all, if you want to have the best ever performance for a single thread, you'll pay the price in pure cost, surface, complexity, vulnerability...

But if you want to KISS, stick to "Emit is Commit" : any instruction that enters the execution pipeline (operand read, operation, writeback) is valid and will complete. This forces the ISA to be adapted to make all instructions conditional to their execution being legal, and the operands to be ready: a scoreboard does that. FC0 also keeps hidden flags to tell the decoder that a read from memory to a register is complete, so the instruction decoder may stall.

This simplifies trap handling and pipeline flushes considerably since there is no pipeline flush, and the pipeline can remain shallow, which reduces the need for a branch predictor.

I think the "original sin" was the early RISC CPUs that included data cache lookup inside the pipeline, making it vulnerable to flushes upon page fault. Load/Store operations were efficient back in the day, until the balance between core and memory dramatically changed. At least with the FC0, several memory fetches could be initiated in parallel, and the instruction was blocked with a page fault was detected or when data is not yet fetched from outside the chip. And there is no pipeline state recovery either.

.

.

Here are some replies for Duane :

OOOC (à la CDC6600) can "retire" instructions out of order because the operands are all available and the result will update the scoreboard during writeback. This will then free the operand for other dependent instruction, whatever the operation's latency : a typical example is ADD vs MUL, which allows some code scheduling tricks described at http://ygdes.com/dct_fc0/dct_fc0.html

(For now I will skip the issue of having to write several values back simultaneously in the register set)

OOOC does not care about the actual latency of the operation : we can simplify the system when all the latencies are fixed (like 1 cycle pour ADD, 4 for MUL...) but the scoreboard system can handle variable latencies, for example a word that must be fetched from L1, L2 or external bulk RAM. This also includes page table walks. But the PT lookup could trap.

.

Overall, if we apply the TTA (Transfer Triggered Architecture) principles,

.

But what about LOAD/STORE instructions ? Although the canonical MIPS archi makes them trivial, they are not at all. In fact their operation could almost be microcoded in the edge cases (RISC uses actual privileged code for this). OOO cores split the STORE instructions into 2 µops, one for the data dependency and another for the address dependency.

"Emit Is Commit" also applies to other control instructions, such a IO or privileged operations. But LOAD/STORE is a pretty wicked case.

Discussions

duanebsand wrote 6 hours ago • point

The Am29000 microprocessor RISC indeed had only a register-indirect address mode on load/stores.  Gemini was hallucinating when it claimed otherwise.  Yay for sometimes-right dusty human memory.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 6 hours ago • point

Books will always be our friends.

Register-indirect more is when you tell which register has the target address, right ?

This is indeed adapted to split-operation : during decode, the Load or Store checks a flag of the address register. If it is already marked "OK" then proceed. If it's "unknown", do the TLB lookup dance and all, and stall until you can check If address is already considered "wrong" so you can trap.

But this mean that an address lookup/TLB check is already executed. Which is why FC0 instruction had a 2nd half to "prefetch" the next address.

  Are you sure? yes | no

duanebsand wrote 6 hours ago • point

Your separation of load/store ops into pairs of "address-gen op; register-indirect load/store op" reminds me of some early risc chip, whose only load/store address mode was register-indirect.  This pattern better fit their ALU and register file pipeline than the usual combined load/store, and matched typical pipelined microcode engines.  I thought this was AMD or Motorola's, but Gemini claims today that Am29000 and M88000 both had conventional multiple address modes with own adder.

Anyhow, I guess you are putting the TLB lookup work into the address-gen instruction, not in the cache access load/store instruction.  I think the generality of doing repeated TLB lookups is needed & useful only when chasing wide pointers that potentially lead to large heap memory.  Global scalars and stacks can be in a small part of the address space that is never paged out when the thread is active.  Or they can share a single large-page tlb entry.  If TLB lookups are an explicit separate instruction, it could use the main innermost dcache rather than requiring its own dedicated extremely fast cache array.

This morning I came across a first-risc essay that called the CDC 6600 "SAn" opcode an auto-increment load/store instruction.  Getting loops' address gen and load/store actions into a concise & fast single instruction.  I suppose it was.  But it could do more address-gen tricks than just array strides, and it worked very much like the "SBn" instructions.  The added register port needed for memory data was cleverly in a separate register file.

Most fast cpus don't wait for memory stores to complete.  The (validated) address and the data operand are dumped into a many-entry queue of pending writes.  That queue is emptied in program order (?), but not at same rate as the separate visible retirement of all instructions.  New loads and stores compare their addresses to all those in the pending writes queue, with stalling or forwarding or merging of results on matches.  A kind of special purpose FIFO cache.  In your designs, can writes complete out of order?  I think your architecture is applying that kind of queuing to all issuing of all instructions? 

I ready today a summary claiming how CDC 6600 exchange jump worked.  It indeed totally drained all its pipelines and queues before restarting the pipes on a new thread's register values.  Much like external interrupts of other machines, just tuned for infrequent events.  If a machine had a single long pipeline, maybe interrupts and traps could be processed with a small pipeline bubble instead of a full drain?

  Are you sure? yes | no

Yann Guidon / YGDES wrote 6 hours ago • point

"Global scalars and stacks can be in a small part of the address space that is never paged out when the thread is active."
This is what I advocate with POSEVEN, where the data memory space is split into 4 sub-spaces with their own "base+limit" registers and larger pages than 4KB. And the actual "control stack" holding instruction addresses is totally separate and unreachable to prevent control flow hijack.

"Or they can share a single large-page tlb entry. "
well, then, how do you catch all data stack overflows and underflows ?

  Are you sure? yes | no

Yann Guidon / YGDES wrote 4 hours ago • point

"In your designs, can writes complete out of order?"

I guess so, following the "weak memory ordering" approach of Alpha (and ARM ?), meaning that a memory barrier instruction is required.

It's yet another smelly can of worms.


"I think your architecture is applying that kind of queuing to all issuing of all instructions? "

well there are "several" architectures, from most basic to more elaborated. YGREC32 has one cache block per pipeline, They serve as writeback buffers as well, so a "walker" scans the entries to preflush the cache lines. The key aspect is to prevent aliasing so a sort of address buffer is required, a CAM shared between the two pipelines to tag the address registers.

There is also a question of throughput, if one pipeline generates addresses and the other performs the actual loads (almost Pentium like) then the maximum average is 1 address per cycle.
The case is still open.

  Are you sure? yes | no

duanebsand wrote 7 hours ago • point

I looked for but did not find, your thoughts on why integer overflow traps are an inexcusable abomination.

From my Pascal background and my Tandem fault-detection background, I lean the opposite way.  Compilers should never substitute checked arithmetic by modulo arithmetic, but instead do modulo math only when explicitly demanded by the programmer.  I also think that byte- and halfword-store instructions should default to trapping if the discarded register bits lose information.  This policy would be fine for most language standards, and upset some non-portable C programs.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 7 hours ago • point

Dear Duane, thank you for all these cans of worms that I'll have to open in even more "logs" and "principles" ;-)

AFAIK only one archi (classic MIPS) does trapping ADDs and even though I could invent justifications, they are more a niche solution, "just one bit in the opcode, please, duuuude" that survived like the Dodo or delayed branches.

Address bounds / checks certainly ring a loud bell for you and it's worthy of the next discussions. I am fond of the approach used by some DSP such as ADi's SHARC but it is not appropriate for "general purpose". A compromise must be found.

  Are you sure? yes | no

duanebsand wrote 21 hours ago • point

Are you saying that load/store ops should not begin doing anything until it is proven that they won't incur a cache miss or TLB miss?  And ADD ops shouldn't begin until it is proven that they won't get signed overflow traps?  I hope you mean that instructions wait on and fetch their register operands before deciding when to permanently commit to their in-order completions.

I prefer the term "pipeline drain" to "pipeline flush".  The prior nearly-completed ops do complete and get delivered; some progress is made.  Every CPU needs a way to cleaning drain pipelines in response to external interrupts; the thread resumption state saved for the OS does not include all pipestage latches.

The brittleness with RISC design is that their ISA was designed around the presumption that innermost data cache would have a usual-case latency equal to just one pipestage cycle.  Possible with early small caches but not later ones.  CDC 6600 was designed for min latency of 3 pipeline/emit cycles during memory reads, but that was also the latency of individual integer alu instructions.  It is interesting that Seymour Cray dumped the scoreboard design when building the 10x faster 7600.

I think you meant "cache miss" and "TLB miss", not "page fault" in describing sinner RISC engine pipelines  No one designs CPUs to make in-thread progress across interrupts for OS-mediated page fault I/Os.  Designers sometimes do try to execute across TLB misses when that table walk is automatically processed by hardware.

  Are you sure? yes | no

Yann Guidon / YGDES wrote 10 hours ago • point

I have agree partially with you, except in particular that ADDs that trap are an inexcusable abomination :-D

And as noted in the updated page, there is no need for in-order retiring in OOOC. In-order retirement (?) is necessary for OOO because of the need to update the shadow instruction pointer.

Yes, the canonical MIPS presumes that one L1 cache access is one clock cycle and the presumption quickly exploded, leading to the "inevitable" OOO designs that somehow manage to make microcode great again. The deep flaw got carried over to the RISC-V family and the price is steep.

You owe us all a very detailed report on the 7600. Curious Marc and/or Master Ken could help !

  Are you sure? yes | no

Yann Guidon / YGDES wrote a day ago • point

There is a similar principle in interactive software architecture.

Coincidence ? I doubt !

  Are you sure? yes | no