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,
- Classic OOO performs the opcode "retire" at the end of a large buffer of in-flight instructions (with a HUGE dependency on the branch predictor's efficiency)
- OOOC "retires" instructions directly from the instruction stream and can solve branches way earlier
.
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.
- FC0 uses a scheme where a LOAD or STORE instruction overlaps the next access, so the first part (data) actually commits the previous load/store instruction, and the other half prepares and checks the address next operation. So it "looks" like load or store but the instructions are actually split and the opcodes can be caught and trap at decode time, if the previous load/store using that register has a problem. That was considered as a "reasonable compromise" to the compiler people who didn't want to go out of their comfort zone, yet it ensures "Emit is Commit".
- All later archis used pairs of address/data registers and dumped the load/store instructions altogether, considerably simplifying the execution and decoding units. The system is close to the CDC6600 except I don't use separate write and read registers (which may or may not be optimal, it's a separate discussion).
The program starts with writing an address to the address register, triggering an address lookup and TLB check. The next instruction that will write or read the linked "data" register will be locked by the scoreboard until the situation is clear : the status of the data register will go from "pending" to "OK" or "Trap" so the decoder does not emit-and-commit the instruction that uses the result of the data register as an operand for a new operation for example.
"Emit Is Commit" also applies to other control instructions, such a IO or privileged operations. But LOAD/STORE is a pretty wicked case.
Yann Guidon / YGDES
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.
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
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
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
"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
"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
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
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
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
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
There is a similar principle in interactive software architecture.
Coincidence ? I doubt !
Are you sure? yes | no