An 8 wide front-end for ARM64 needs to run a decoder every 4 bytes (so 8 in total), then combine the results. It's an entirely trivial affair.
For RISC-V, without any macro-op fusion, you first need a unit every 2-bytes that determines the instruction length using the first 2 bits (16 of these), then you need a very wide mux to expand out any compressed instructions (the last one can come from 7 or 8 different offsets) so you have 8x 32b instructions, then you need your 8 decoders. It's looking much more unpleasant because of that very wide muxing (8x 8:1 32-bit muxes is not a pleasant sight). Alternatively you could do it the brute force way and have 16 decoders followed by a 16->8 reduction layer (ignoring the decode results that were at bad offsets). The designer is going to have a hard time choosing between those two bad options (personally I would go with the latter I think).
Now let's throw in macro-op fusion, suddenly rather than have two possible lengths (16b or 32b) you can have 16b+16b, 32b+32b, 16b+32b, 32b+16b. Complexity just explodes. If anybody really tried to implement a 8 wide RISC-V front-end with pervasive macro-op fusion then it would be a truly nasty thing.
This also avoids the need to macro fuse cmp+jmp, but OTOH eats quite severely into the destination offset.
However given they are already paying the cost of a variable length instruction set (for compression) I think a wide set of reg/reg, reg/imm8, reg/imm16, reg/imm32 compare+jumps would have been even better. Right now compare+jump against an imm32 is 3 instructions (4 if it’s long) vs 2 for x86, that number could have been 1 (given variable length).
Personally I think only two good choices exist for instruction sets, fully fixed size at 32b or variable with 2b control (max 2 inputs and 1 output reg!) followed by 0b, 2b, 4b, etc data (and thus very powerful instructions like the aforementioned). Anything else seems to be off the pareto curve. Goodbye cmov.
Besides fixed-size instructions and the traditional variable-size instructions, one can do variable-size instructions in bundles. An example would be 25-bit and 50-bit instructions packed into 128-bit bundles, with the remaining 3 bits used to specify all the sizes. (eight patterns: nnnnn, nww, wnw, wwn, nnnw, wwnw, wnww, wnnn) Extending that out to a typical cache line of 512 bits might be better. Another option is to use 1 of every 16 bits to indicate where instructions start.
Where RISC-V got wasteful was the registers. Compilers are seldom able to use anywhere near 32 registers. On normal code, normal compilers seem to need about 8 to 10 registers free after deducting the ones reserved by the ABI. The ABI might need 3 to 5 registers. (stack, PLT, GOT, TLS, etc.) That means that roughly 11 to 15 registers are needed. Clearly, 4 bits (16 registers) is enough. Shoving some of those ABI-reserved registers out of the general-purpose set wouldn't be a bad idea; most of those are just used for addressing.
You mean things like having variants of common arithmetic instructions that update or don't update flags?
> Besides fixed-size instructions and the traditional variable-size instructions, one can do variable-size instructions in bundles. An example would be 25-bit and 50-bit instructions packed into 128-bit bundles, with the remaining 3 bits used to specify all the sizes. (eight patterns: nnnnn, nww, wnw, wwn, nnnw, wwnw, wnww, wnnn) Extending that out to a typical cache line of 512 bits might be better. Another option is to use 1 of every 16 bits to indicate where instructions start.
Yeah, something like that could be nice. Though how would jump instructions be encoded? Bundle + offset within bundle?
> Where RISC-V got wasteful was the registers. Compilers are seldom able to use anywhere near 32 registers. On normal code, normal compilers seem to need about 8 to 10 registers free after deducting the ones reserved by the ABI. The ABI might need 3 to 5 registers. (stack, PLT, GOT, TLS, etc.) That means that roughly 11 to 15 registers are needed. Clearly, 4 bits (16 registers) is enough. Shoving some of those ABI-reserved registers out of the general-purpose set wouldn't be a bad idea; most of those are just used for addressing.
Nah, I think 32 registers was a good choice. (Relatively) common loop optimizations like unrolling or pipelining need more registers. Also, some of those registers are callee saved and some are call clobbered; by making use of this information the compiler can avoid spilling and reloading of registers around function calls.
For x86-64 16 registers is fine, partly because in many cases one can operate directly on memory without needing to explicitly load/store to architectural registers, and partly because the target was and is OoO cores that aren't as dependent on those register-consuming compiler optimizations.
Bundle + offset is fine. The offsets don't need to be real. In the example given with 25-bit and 50-bit instructions, allowable low nibbles of instruction addresses might be: 0 1 2 3 4 (so it goes 0x77777773, 0x77777774, 0x77777780, 0x77777781, etc.)
I disassemble binary executables as my full-time job. I've dealt with over a dozen different architectures. I commonly deal with PowerPC, ARM, MIPS, x86-64, and ColdFire. The extra registers of PowerPC and MIPS are always wasted. Even with ARM and x86-64, unused registers are the norm. It simply isn't normal for a compiler to be able to make effective use of lots of registers. Surely there is an example somewhere that I haven't yet seen, but that would be highly abnormal code.
If more registers could be used by compilers, the Itanium would have been a success.
> If more registers could be used by compilers, the Itanium would have been a success.
I kind of feel the Itanic never made it far enough for its register count to have mattered to anyone. I wonder if SPARC would be a better comparison... it was somewhat popular in the 90's and 00's, and was a RISC chip with oodles of registers, wasn't it?
Well ultimately they do, not updating the flags means more options for the compiler (things can be scheduled in between the compare and jump), although with cmp+jmp fusion that's now a bad idea making the concept dates.
Ultimately each instruction pending execution in an OOO core needs to sit somewhere waiting for its inputs to be available. If you are x86 and you suggest cmov then potentially you need to wait for 3 registers and flags, meaning every slot in this structure needs to be capable of waiting for 4 things to happen before becoming ready. In RISC-V you only need to wait for 2 things for any instruction.
Presumably some of the 4-bit prefix patterns would indicate that the final instruction in the bundle was an immediate value instead of an instruction.
Alternatively, one could take a page form Sun's MAJC playbook and shorten the opcode of the first instruction in the 64-bit bundle by 2 bits to indicate 4x16-bit, 2x32-bit, 16+16+32-bit or 32+16+16-bit instructions in the bundle.
BTW your analysis ignores that in the compressed ISA you can have 4-byte instructions aligned on a 2 byte boundary
Actual real world performance tests already suggests that RISC-V tend to require less memory for their programs as well as fewer instructions than x86 and ARM. Hence even without pervasive macro-op fusion they are in a good spot.
E.g. for CoreMark used to benchmark embedded processors RISC-V BOOM implementation came ahead ARM-32 Cortex-A9 with about 10% fewer instructions, 10% smaller memory usage and about 10% higher clock frequency (thanks to smaller core).
In addition RISC-V has twice as many registers as ARM-32 Cortex-A9, which the particular CoreMark was comparing against. Also a different ABI conventions reduce the number of instructions spent on saving and restoring registers. E.g. RISC-V has certain registers marked as saved, temporary, arguments etc.
Then of course for RISC-V it isn't expensive to support compressed instructions. You use the same decoder not different modes and different decoders like ARM. Just 400 gates to implement, so it is kind of a no-brainer.
If comparing SPEC CPU2006 benchmark code using GCC compiler then uncompressed RISC-V vs uncompressed ARM is basically the same. Compressed RISC-V and compressed ARM is basically the same. The latter beats x86 by about 26%.
Uncompressed RISC-V is just 8% bigger than x86. So I would say in general RISC-V is paying a very small price for its simplicity. One could say there is a benefit in being able to learn from past mistakes.
seems to show that RISC-V has code density ~11% worse than ARM (although against Thumb2, so not 64-bit)...
Status bits have turned out not to be such a problem as was once thought. Nowadays they just rename status registers along with the others.
Perhaps RISC-V's worst choice was the coarse-grained extensions architecture that locks up the most useful instructions with dozens of marginal ones, making them unusable in portable compiled code.
As a result, no RISC-V program can assume POPCOUNT is available, because it is locked away in the huge (still-unratified) "B" extension.
The most difficult problem modern CPU designers face is coming up with a way to put another hundred-million transistors to productive use -- i.e., that will make programs actually run faster, but without running too hot. Modern chips are crammed with dodgy, poorly documented gimcracks that make programs run absurdly fast much of the time, but not always.
Programmers now have two problems: discovering whether their programs actually are running fast, and discovering why the ones that aren't don't. Usually the only way to know our program is slow is when we see a faster one. It was fast until exactly that moment, then became instantly slow.
Yes, this is the unintended consequence of trying to combine minimalism with too much modularity. Ironically this ends up with a more bloated core. Or perhaps the modularity is probably designed to provide more differentiation.
Need POPCOUNT, have to implement the B extension. Need vectors, have to implement scatter gather. Also Can't merge the register file with the FP registers.
In reality what will happen is people will choose instructions from these extensions and implement them as custom isa.
This will lead to a lot of non-portable code. Until some one combines all these useful ops into another standard extension. Repeat.
If you want to only have core + popcount you can do that in your own core. The code will still be portable to all cores that have B extension. If there is a whole industry that needs that combination, it can just be its own profile.
There simply is no perfect solution for every possible solution. If you increase the core, you hurt those that have absolutely no use for some of these things.
RISC-V did an amazing overall job to find a combination of ideas to attempted to solve everything for all people but the reality is, no single thing they could have designed would not end up with some expert on HN explaining why the choice is wrong.
Its not yet complete and eventually for desktop a more extended profile will be needed, but that profile will be the standard that all open source distros will build around, just as now RV64GC is the standard profile.
I don't understand why you even think the base spec should cover all things need for desktop, that was never the intention. In fact it was exactly not the intention.
RISC-V ecosystem explicit goal is to work form the smallest embedded core, to the largest super computer.
Also, you mentioned elsewhere that you're a hardware designer. I'm aware that split register files are advantageous for speed, while unified integer and fp register files are advantageous for small/low power implementations. Given that the high-performance implementations are going to use register renaming anyway, has anyone quantified the penalty in using register renaming to allow for a split register file in high-performance implementations while allowing compatible low-power implementations with a single register file? Maybe the transistor savings are dwarfed by other concerns in a modern hardware implementation, but given RISC-V's academic goals, economy for FPGA implementation seems useful.
I suspect mixed-width streams are extremely common (at least going by compiled RISC-V code I've seen). Potentially you can alter compilers to change this but the compressed instruction set is very limited, it naturally mixes compressed and uncompressed and forcing otherwise will likely produce poorer code overall.
I'd also note that branch prediction accuracy is a key part of modern processor performance, they're maybe around 95-99% accurate most of the time. Your scheme looks to have similar mispredict penalties to branch prediction (you have to chuck things away and start over, though at least you don't have to refetch) so will likely require similar level of accuracy to work well. Plus on any mixed stream performance will be very poor as you've explicitly not built a decoder for mixed streams, so you end up dripping things through one at a time or you allow limited decoding on mixed streams (maybe 2 instructions per cycle).
Edit: Perhaps the mis-predict penalty isn't so bad compared to a mis-predicted branch as detecting the mis-predict is trivial. Indeed having just realised this you may be better off without the predictor at all, just look at the bytes and decide which you should do.
Though depending on the decoders you may end up needing two entirely separate sets of logic for the compressed instruction decoder vs uncompressed instruction decoder anyway, in which case you just decode your stream in both ways then decide which set of decodings you want to use. You still have the issue with any mixed width stream killing your performance though which I suspect would be a major issue with this design (so much so it's not useful).
I'm not sure I fully get your point. A low power implementation would have one physical register for each register in the instruction set (ie. for RISC-V it's already split). If you have a unified file in the instruction set then that means that you're going to have fewer GPRs available (ok for RISC-V as 16 is enough really) and you either need unified in your OOO implementation or you need to make integer->floatingpoint forwarding an exception (at which point you'd be better just to split 16/16).
My understanding is that the designers of both POWER and aarch64 did a bunch of research before settling on 32 for the number of gp registers, and there are some workloads where 32 are really needed, so having a fixed split is suboptimal for some workloads. Aarch64 isn't that old, and I'm not aware of major improvements in compiler register allocation algorithms since it was designed. Are you contending that ARM made a mistake in their analysis when expanding from 16 to 31/32 gp registers for aarch64?
So, my assumptions are (1) optimal split between fp and gp depends on workload (2) splitting them has a cost in die area for low-power implementations and (3) high performance chips can use the register renaming hardware to get the extra register file read ports from split register files and effectively tailor the gp/fp split to the workload. You wouldn't need to make integer-fp forwarding an exception, though the forwarding might have latency similar to an L1 cache read. Presumably, the register renaming hardware would keep a single bit (or small 2-3 bit saturating counter) to keep track of the last usage of each register, so that load instructions were likely to be stored to the optimal register file.
It just seems that if you're going for a brand new green field architecture design, you can get rid of the cost of a split register file for low-power implementations and get all of the benefits of a split register file in high-performance designs that are going to have register renaming hardware anyway.
Maybe I'm missing something with regard to your comment about integer-fp forwarding needing to be an exception instead of incurring slight latency.
On the register file split I think you're generally right, it just comes down to what the penalties actually look like from having the unified file. I think the loads are a cause of trouble, really you need two different types of loads rather than having a predictor I think (low-power implementations could ignore that single bit). It's really just a matter of those register file read ports, you are going to need a lot more or need less backends or get bottlenecked on read ports (remember not everything gets forwarded, much is already retired).
One thing that's also relevant to the discussion is almost always when you add FP to a CPU these days you also add some kind of vector ops, and given vector registers are not the same length as the GPs you can't have a unified file. If I was doing green field I would probably go for a split file, but then say low power implementations shouldn't implement FP/vector at all (which is what RISC-V has done), unfortunately although this idea is popular with hardware designers it is not with software developers who are used to being able to drop a 'float' or 'double' into their C code and have it work (or worse may use a library that includes floats in its internals).
There has been some talk of a breaking fork of RISC-V better suited to large high performance designs. I think that makes a lot of sense and would allow RISC-V to stay good at what it was intended for.
Also, RISC-V is also essentially split into two incompatible instruction sets (32-bit and 64-bit, even if you forget the huge amount of extensions). It would have been better just to design two completely separate instruction sets (32-bit for low power, 64-bit for high performance), but at core it's a teaching instruction set like MIPS before it, so they went for simplicity not optimality.
>First it is combining two instructions into one through compression.
>Then it splits it into two through decompression.
>The combine them back into one operation through macro-op fusion.
The general pattern appears to be: load 8 32bit instructions (2 compressed instructions count as 1 32bit instruction), convert them to up to 16 32 bit instructions. Now your macro op fusion would only have to operate on 32bit instructions.
Although the decoder sounds trivial the macro op fusion still has a massive problem. x86 macro op fusion only needs to look at the first instruction to know which macro op fusion to apply because x86 does not fuse more than 2 instructions.
The type of macro op fusion that RISC-V needs to be competitive would span up to 6 instructions. At that point your instruction decoding problem is as complicated as a regex which means unbounded complexity compared to whatever x86 does. Solving this problem efficiently would require some sort of "hint prefix" on the first instruction. This is far more complicated than just adding a new instruction.
It's common practice that CPU designers actually evaluate every possible choice in an emulated environment, and make a quantitative analysis, before actually choosing to go down a particular path. At least, that's typically part of CPU design courses. This is the game that has allowed Apple to make a major leap in a short amount of iterations, and other CPU designers should become better at it. We can discuss all kinds of optimizations, but before we throw them in a model there's little we can say about their effectiveness, while the model is a very cheap way to learn more.
I do think that if someone wants an 8-wide RISC-V decoder all they have to do is throw the transistors at it. That's a consequence of fixed width. Yeah, RVC will require more transistors, but not n^2 (?) like X86 does.
RISC-V is pretty elegant, well except for the bit manipulation nonsense. They really should have plagiarized ARM instead of Intel there.
The fact that risc-v only supports 32,64, or 128bit instructions means that you don't have to go through a lot of extra effort to determine where instructions start or end.
Variable length isn't actually a huge problem at the instruction fetch level (and modern compilers will align x86 instructions to keep ifetch moving smoothly), but it does make both hardware and software decoding significantly more complex.
But since I'm not a hardware designer, I don't know if the RISC-V design is enough to make a high-performance wide decoder. With 64-bit ARM it seems very easy; once you loaded a n-byte line from the cache, the first decoder gets the first four bytes, the second decoder gets the next four bytes, and so on. With compressed RISC-V, the first decoder gets the first four bytes (0-3); the second decoder gets either bytes 4-7 or bytes 2-5; the third decoder can get bytes 8-11, 6-9, or 4-7, depending on how many of the preceding instructions were compressed; and so on. Determining which bytes each decoder gets seems very easy (it's a simple boolean formula of the first two bits of each pair of bytes), but I don't know enough about hardware design to know if the propagation delay from this slows things down enough to need an extra pipeline step once the decode gets too wide (for instance, the eighth decoder would have 8 possible choices for its input), or if there are tricks to avoid this delay (similar to a carry-skip adder).
As a side-note carry-skip doesn't really work in modern VLSI, I guess you were probably thinking of carry-lookahead.
But I might be wrong about this. Do you have any source that suggests that instructions of different length should be mixed in one RISC-V program other than use of compressed instructions?
"The base RISC-V ISA has fixed-length 32-bit instructions that must be naturally aligned on 32-bit boundaries."
Later it talks about:
"For implementations supporting only a base instruction set, ILEN is 32 bits. Implementations supporting longer instructions have larger values of ILEN."
It seems clear to me that a standard RISC-V implementation today is 32-bit fixed sized on a 32-bit boundary. There may however be support for future architectures with longer instructions. None of this suggests that a regular RISC-V implementation has to assume that instructions can be any length.
These things are not even part of the standard yet. So please, don't be such a dick about something that isn't all that clear at the moment.
https://www.embarcados.com.br/wp-content/uploads/2017/05/RIS...
It is a very simple circuit that can take the bottom 16 bits of any RISC-V instruction and tell you exactly how long it is, unlike the x86 where you need a sequence of steps.