4 ms·
The RISC-V authors rely heavily on the argument and assumption that any high performance implementation will do instruction fusion, and the lack of indexed addr
by mbitsnbites 3y ago
The RISC-V authors rely heavily on the argument and assumption that any high performance implementation will do instruction fusion, and the lack of indexed addressing is precisely one of the things that is expected to be fused (see page 6 in https://people.eecs.berkeley.edu/~krste/papers/EECS-2016-130.pdf https://people.eecs.berkeley.edu/~krste/papers/EECS-2016-130...).
Any implementation that does not do instruction fusion (most implementations - especially lower end ones) are out of luck, and have to pay the penalty of executing extra instructions.
All modern high performance ISA:s include indexed addressing modes (most also have scaled indexed addressing). Some RISC-V implementations add these as non-standard extensions.
RISC-V was not designed primarily for high performance. It targets the extremely low end (education, microcontrollers), and does it really well, and they have this clever trick (compression + instruction fusion) that makes the situation OK:ish for high end implementations. But it's not ideal.
Also worth noting is that instruction fusion is far from free. It may be negligible in a really big implementation that already does things like instruction translation (uOP caches etc), but it sure feels like an unnecessary burden to bring to an ISA that hasn't even had the time to build up a legacy baggage yet.
Furthermore, fused instructions often "leak" data to architectural registers (e.g. temporary registers used for address calculation), which both increases register pressure and requires that fused internal instructions must produce extra outputs (often unnecessarily).
Edit: Consider the following code (https://godbolt.org/z/8YdfoT34o https://godbolt.org/z/8YdfoT34o):
void store(int* array, int index, int value) {
array[index] = value;
}
MRISC32:
store(int*, int, int):
stw r3, [r1, r2*4]
ret
RISC-V:
store(int*, int, int):
slli a1,a1,2 // writes to a1
add a0,a0,a1 // writes to a0
sw a2,0(a0)
ret
Note how the RISC-V version clobbers both the A1 and the A0 registers. Thus, a properly fused instruction would have to read from three registers and write to two register (that's bad!), whereas the MRISC32 solution only has to read three registers - no registers need to be written to (same thing for ARM and others). (You could teach the compiler to do an "add a1,a0,a1" instead so that only one register is clobbered, but that adds complexity to the compiler, and the micro architecture may not be able to rely such compiler optimizations but would most likely have to support the case with two clobbered registers anyway).
Edit 2: Another main reason for not including indexed addressing modes in RISC-V is that indexed stores require three source registers (i.e. at least three register file read ports are needed) - which is a big no-no in the RISC-V philosophy. For any implementation above the purely educational level, 3+ register file read ports is the norm, but AFAIK the RISC-V authors have claimed that the base integer ISA will never require more than two source operands. I think that this philosophy weighs very heavily - more so than optimal performance.
- dzaima 3y agoMinor note - RISC-V's Zba (included in RVA22, which afaik is set to be the baseline requirement for Android) has sh2add for a combined x+y*4, getting that store down to two instructions (testable with -march=rv64gczba); doesn't affect the overall point of needless clobbering of registers from the instruction fusion approach though (and shouldn't indexed loads be even worse, having three potential output registers in baseline RV64GC, and still two with Zba?)
- mbitsnbites 3y agoFor loads you can usually hide the clobbering by using the intended target register (the one you want to load to) as the temporary register, so e.g: // a0 = load [a1 + a2*4] slli a0,a2,2 add a0,a0,a1 lw a0,0(a0) ...but not always.
- dzaima 3y agoThat's the same approach that gets you from 2 to 1 clobbered registers for stores, no? i.e. usually possible, but needs compiler participation; if assuming the compiler will optimize load register usage properly, it shouldn't have any problem reducing the store clobbers from 2 to 1 either, which I understood as the difficult part. (or course 1 clobber is still worse than 0, but at least it means no 2 writes/fused instr, at least here)
- mbitsnbites 3y agoTrue, but for loads there is at least a theoretical way to avoid clobbers. For stores there is no way. Stores: 1-2 superfluous results Loads: 0-2 superfluous results
- brucehoult 3y ago> store(int*, int, int): > slli a1,a1,2 // writes to a1 > add a0,a0,a1 // writes to a0 > sw a2,0(a0) > ret Three points: 1) you could argue that's bad code generation, which could easily be fixed in the compiler, as `a1` could be used as the result of the `add` 2) since late 2021 RISC-V has the `sh2add` instruction (and shifts of 1 and 3, and also versions that widen and shift a 32 bit unsigned value on a 64 bit CPU) that replaces the `slli` and `add`, thus naturally clobbering only one register. These instructions are already present in the popular RISC-V boards that use the JH7110 SoC (VisionFive 2, Star64, Mars, PineTab-V, Roma) 3) using a function like this to prove anything actually verges on dishonest (and erincandescent did the same thing) because no real program where anyone cares about performance will have such a function in hot code. If you care about the performance it's going to be because it's in a loop, and the function will be inlined, and strength-reduced, and the actual code will look like: sw a2,(a0) addi a0,a0,4 I have in the past proposed adding simple reg+reg indexing to loads (only), as that fits within the 2r1w framework and loads are more numerous than stores. Similarly, stores could write the effective address back to the base register, which would also fit in the 2r1w framework. This would allow something like ... void arrAdd(int *dst, int *src1, int *src2, int n) { for (int i=0; i<n; ++i) dst[i] = src1[i] + src2[i]; } ... to be compiled as ... arrAdd: sh2add a3,a3,a0 // just past end of dst addi a0,a0,-4 // bias for incr by 4 in the store sub a1,a1,a0 // bias for indexed addressing sub a2,a2,a0 1: lw a4,(a1,a0) lw a5,(a2,a0) add a4,a4,a5 swwb a4,4(a0) // store AND a0 = a0+4 blt a0,a3,1b ret This is the same number of instructions as now (no code size saving) but three fewer instructions in the loop. > For any implementation above the purely educational level, 3+ register file read ports is the norm, but AFAIK the RISC-V authors have claimed that the base integer ISA will never require more than two source operands. I think that this philosophy weighs very heavily - more so than optimal performance. It gives optimal price-performance. Having three read ports is rather expensive, and nowhere near justified by the number of integer instructions that would ever actually make use of the 3rd port, even if you added indexed stores, conditional selects, double-width shifts with dynamic shift count, multiply-add. Three read ports are justified in FP code as multiply-add is the fundamental and by far most popular instruction in FP code and besides IEEE rules require a single rounding in a multiply-add.