I am often asked to explain my distaste for RISC-V and I often find myself explaining it piecewise. The reactions are often of the form “you just do not understand the brilliance of it all”, which is, of course, no argument at all. After being asked for the Nth time to explain, I decided to put it all down in one place so that I could simply link to it when asked next. Plus, if anyone wishes, then, to form a coherent counter-argument, they could refer to my points clearly and in detail by having this text as a reference. All opinions stated here are mine and do not represent the views of my employer, any deity, or my landlord. My cats concurred in part and dissented in part and will publish their opinion later.
The first and simplest-to-grasp issue is that one cannot be best for all use cases. RISC-V fans would have you believe that RISC-V will soon own all supercomputers, while also owning all the tiny microcontroller use cases, and all things in between. This is impossible, and would be equally impossible for any ISA. Simply put, the things a high-end CPU needs are diametrically opposed to the things a small cost-saving microcontroller core needs. The design choices are not merely microarchitectural, but actually (and necessarily) impact the CPU architecture itself. For what it is worth, I am 100% sure that RISC-Vwillown the cheap-as-dirt single-use microcontroller space eventually. Not due to its ISA design, but despite it. It will take this role from 8051 by being an improvement on it — a bar so low, it is but a speed bump.
What does a cheap microcontroller core need? Let’s inspect what they are used for. Typical use cases are to interface with and quickly reconfigure hardware blocks in a larger chip, eg in an MP3 player, an SD card, or a USB stick. The hard work is done by custom IP and the CPU core is just there to occasionally prod a register or configure something. What matters in this case is interrupt latency (lower is better) and size (smaller is better). Usually you would not expect much math to be done on such a core. Mass-produced cost-reduced devices would have the code running out of real ROM (if non-updateable) or RAM (if updateable); NOR flash costs too much and is not an option for really-mass-produced things. When running out of ROM, code size matters because ROMs are not very compact. When running out of RAM, code size matters because SRAMs also take up a lot of space on the die. Thus, code density matters for these use cases. Since much math is not expected, things like hardware dividers (or even multipliers) can be discarded. Privilege separation is also not needed in such single-use situations — no external untrusted code is expected to ever be fetched. “But, ” you might say, “you just describedRV32IC(orRV32EC)!”
Indeed, it is somewhat close, except really you needRV32I_Zicsrto claim that. WithoutZicsr, there is no spec-compliant way to handle interrupts, as there is no temporary place to stash a register to allow you to stash the rest of them. MIPS reserved two kegs for this ($k0and$k1). Without them, RISC-V needsmscratch/sscratch. WithoutZicsr, you do not have those and are stuck with weird other methods to do things. And thus we are back in 8051 territory – it specializes in doing things weirdly. Small embedded cores are not out-of-order monsters. If you get one instruction per cycle out of them, you consider yourself lucky. Given this, let’s optimistically count the number of cycles needed for an interrupt handler to stash ABI-required regs and call a handler written in C. First we’ll use aCSRRWto stash a reg (let’s sayt0for ease of explanation) and get a base address of where we may stash the rest. Then we’ll need to stashra,sp,gp,tp,t1-t6anda0-a7. We’ll then need to use anotherCSSRWto get back the oldt0value and stash that as well. That’sat least21 cycles. On the way out, the math is similar: oneCSRRWto read the address of the stashed regs, and 19 loads to load them. That’sat least20 cycles. But that is not all. Since this needs to be done in assembly, we’ll need to actually account for theJALto our C handler and aRETfrom there. We’ll graciously assume those are each two cycles. Thus each interrupt has at least a 44-cycle cost before any work is done in the C handler. Cortex-M0 (the competing cheap 32-bit core) does an interrupt entry in 15 cycles, exit in 12 cycles, and since it pushes the ABI-clobbered regs in hardware, the handler is written in C directly. Thus each interrupt here has only a 27-cycle cost. Oof… that’s a lot faster! You might protest that I am being unfair by not consideringRV32Ehere. By having half as many regs, it can do the initial push 6 cycles faster and the pop as well, bringing its interrupt overhead to 38 cycles. Still over a third more than the Cortex-M0. Oof… So, at basically the only purpose such an embedded core has, RISC-V is notably worse than the leading existing competitor. The existence ofCLICandvarious proprietary “fast IRQ” / auto-stacking extensions is an additional indictment. The base ISA forces vendors to invent non-standard silicon to reach parity with a decade-old Cortex-M0. That, in turn, further fragments the “standard” (if it can so be called). Hilariously, even with the compressed extension, the typical IRQ prologue is larger and slower than the Cortex-M0’s zero-byte hardware path.
Now, about those compressed instructions. Let us look at them in detail. They are hilariously poorly designed. Say you want to store a byte to a register plus offset. What range of offsets can a 16-bit instruction encode? Zero through three. Not thirty three, not three hundred and three. Three! Well, maybe it is better for storing a halfword? Nope… zero or two. What even? Why? At least when you store a word, you get a sane range of zero through 124 bytes, but what is going on there with those other ones? Worse, the instruction for storing a halfword is encoded similarly to the one storing a byte, but somehow it has fewer options for offsets? Why? Well, one of the bits that store-byte uses for offset is just hardwired to zero… it could have been used to expand the range to at least go to 6, but it doesn’t! By comparison, Cortex-M0 is happy to let you use offsets from zero to 31 for bytes, zero to 62 for halfwords, and zero to 124 for words – clearly this covers a lot more use cases. So what happened here? Truly, I do not know, but it is indeed hard to justify. A typical refrain is to just use full-length instructions for these larger offsets. Sure, but density will suffer – the very density that RISC-V fans were bragging about so recently when trumpeting theCextension. But wait, there is more yet. Those instructions to store a byte and a halfword are not evenintheCextension. They are in another one calledZcbso you may not get access to them at all, even if their puny range were good enough to use in your situation. We’ll get to “extensions” later…
What do server cores need? Raw throughput. Here, we are in the world of out-of-order cores where silicon is more or less free, since no matter how big your core is, the caches will dwarf it in size. Modern out-of-order cores are decoding eight or sometimes ten instructions at once, and issuing them to multiple ports concurrently; many modern cores can take more than one branch in the same cycle (think about that for a second, let it sink in … yes). Code density is really not as much a concern here as it was in the past. It matters, but making a slightly larger L1i is not terribly complicated and, again, Si area is more or less free on the scale of such small things. What you really want is the ability to fetch and decode as many instructions at once as easily as possible. While doing that, it also helps if the instructions tell you as much about their intent as possible, to allow you to merge them with others or split them up into pieces most efficiently. Seemingly, these two desires are at odds with each other, and to some extent it is true. “Easy decoding” is, as is widely known, latin for “fixed length” while “as much as possible” is greek for “long”. Obviously we do not want fixed-length very long instructions. Where do we draw the line? Having instructions be a power-of-two in length makes many other things like alignment easier, so then what? Two bytes is too short. Eight bytes is too long. The answer is fixed length 4-byte instructions are a nice middle ground. That provides enough encoding space to encode almost anything you’d want, namely: 3 registers encoded in each instr, long offsets for branches. Why does this sound familiar? Because that is what aarch64 (and A32) have proven to work exceptionally well. MIPS made the same choice for the same reason.
You might now protest that ARM also has Thumb and MIPS has microMIPS. However, in high-performance compute Thumb is dead. WhenApple was designing aarch64 with ARM, much modeling and testing showed it to be a net loss for instructions per watt and instructions per second. MIPS would surely have killed microMIPS too, had MIPS lived long enough to reach the current cost-per-transistor regime. The main upshot is that compressed instructions have no business in large cores, they get in the way of fast parallel decoding of many instructions by making it slower to find boundaries. You might protest that “RISC-V makes it easy to find instr lengths”, but “easy” is not the same as “instant and free” that fixed-length instructions grant you.
Getting back to our discussion of the raw performance that high-end cores need to demonstrate. What is one of the most common operations performed by any code? Array access. This is why x86 has addressing modes of the form[ebx + esi * 4]and ARM has[R0, R1, LSL #2]. Without it, you are forced, like an idiot, to shift a register left by two, then add it to another register, and only then use that to access memory. Three instructions for a single array access. The usual excuse given for this inexcusable lack of foresight is that “instruction fusion will fuse all those three instructions into one in fast cores”. Yeah… if anyone ever pulls that off, they will win many prizes. No core I am aware of fuses more than two consecutive instructions. None. The reason is quite obvious — the combinatorial explosion of the number of possible combinations to consider, track, and handle. So now that we’ve established the bullshit excuse is bullshit, what is there to be done? Well, a fewYEARSafter the spec was written, an extension was proposed to help this issue –Zba. It provides three instructions of the formSHxADDfor x being 1, 2, or 3. This combines a shift with an add, basically shortening our array access from three instructions into two. This is still worse than having register + shifted register addressing mode, but at least now “the core can fuse them” becomes less bullshit and more believable, assuming someone produces such a core. One problem:SHxADDis always 4 bytes long, and the memory access instruction itself will be 2 or 4 bytes, so the array access becomes 6 or 8 bytes of code, to ARM’s 4. This is where all those people who were just shouting about the wonderfulness of theCextension for density and how great it is for high-perf cores quietly shut up and look at the floor. Yeah… For extra credit, theZbaextension was only ratified in 2021, over two years after the base spec. It took themTWO YEARSto realize that arrays exist!
Curiously, this would be easy to fix. Currently 3/4 of the encoding space is allocated to compressed instructions (all instructions whose lower 2 bits are not0b11). Reusing some of that encoding space for better addressing modes is a no-brainer and would produce denser code with better array addressing ability. Unfortunately, it would make too much sense for anyone to actually do. Of all possible champions of sanity, Qualcomm … proposed doing this, and even prototyped it. It went nowhere… And you know you really fucked up bad when you manage to make Qualcomm sound like the voice of reason. But, back to ourSHxADDs. There is no guarantee that you’d get to use them anyways, sinceZbais an extension and is thus optional. Are you getting tired of hearing “optional” yet? Let’s talk about that next.
What does RISC-V have in common with USB-C and RCS? These things are all ostensibly standards, sure, but the interesting part is that claiming to be in compliance with one of these standards means NOTHING while being technically true. Is my USB-C cable wired only for USB 2.0 valid? Sure, USB 3 twisted pairs are optional. Is my non-e-marked cable valid? Sure, e-markers are optional. Can my USB 3.0 USB-C cable choose to not support 20Gbps? Sure, 20 Gbps support is optional. Can it support 20Gbps but not support 100W charging? Sure, that is optional too. Can my phone’s fully-compliant RCS implementation not support upgrading a text message to a video call? Sure! MIVC is optional. Can it fail to send pictures while I am on a call? Sure, that is optional! Can encryption not be supported? You bet, that is optional too! So what does it even mean to comply with the spec then, if everything is optional? It means the spec writers spent too much time engaging in mental masturbation and too little time in contact with the real world, basically. There are two ways this happens: academics who are not aware that outside their offices, there is such a thing as the real world, and design-by-committee situations, where the real world simply never gets a seat at the table — having failed to file a motion to be seated there in time for the chairman to bring it to a vote.
Just scope out this line: “The RISC-VB(Bit-Manipulation) extension is a standard collection of instruction set enhancements designed to improve performance and code density through efficient bit-level operations. It is split into distinct sub-extensions:Zba,Zbb,Zbc, andZbs.” Only a design-by-committee process would ever unironically produce this sequence of words.
When writing a spec, every single thing you make optional, you split the possible implementations into two incompatible groups. Do this enough times and you end up with your spec being meaningless. And boy, did the designers of RISC-V screw the pooch here. Everything is optional! Multiplication — optional. Division — optional. Support for a user mode — optional. Supervisor mode — optional. CSRs — optional. Compressed instructions — optional. “Extra compressed instructions” — still optional. I bet that if they thought they could get away with it, they’d make addition optional!