[HN Gopher] RISC-V Conditional Moves
___________________________________________________________________
RISC-V Conditional Moves
Author : gok
Score : 38 points
Date : 2025-09-29 12:47 UTC (3 days ago)
(HTM) web link (www.corsix.org)
(TXT) w3m dump (www.corsix.org)
| sylware wrote:
| This is implemented with instruction fusion. Just need to
| document properly and publish properly what will end up "standard
| instruction fusion patterns" (like the div/rem one).
|
| Adding more instructions is kind of non productive for a
| R(educed)ISC ISA. It has to be weighted with extreme care.
| Compressed instructions went thru for the sake of code density
| (marketing vs arm thumb instructions).
|
| In the end, programs will want probably to stay conservative and
| will implement only the core ISA, at best giving some love to
| some instruction fusion patterns and that's it, unless being
| built knowingly for a specific risc-v hardware implementation.
| Pet_Ant wrote:
| Compressed instructions are also for microcontroller use.
| RISC-V -rightly or wrongly- is trying to be an ISA that can
| handle the whole stack from embedded microcontrollers to a top-
| end server.
|
| As such, there are compromises for both aims.
| vardump wrote:
| Instruction fusion still means lower code density. You can go
| overboard, but the newer ARM instruction set(s) are pretty
| good.
| Findecanor wrote:
| Not necessarily lower density. On ARM you would often need
| cmp and csel, which are two instructions, eight bytes.
|
| RISC-V has cmp-and-branch in a single instruction, which with
| c.mv normally makes six bytes. If the cmp-and-branch
| instruction tests one of x8..x15 against zero then that could
| also be a compressed instruction: making four bytes in total.
| astrange wrote:
| ARMv8.7 added some new instructions for int min/max to
| replace cmp+csel. (I'm surprised it took them so long to
| add popcnt.)
|
| https://www.corsix.org/content/arm-cssc
| duskwuff wrote:
| As an aside: it's only relevant on microcontrollers nowadays,
| but ARM T32 (Thumb) code density is _really_ good. Most
| instructions are 2 bytes, and it 's got some clever ways to
| represent commonly used 32-bit values in 12 bits:
|
| https://developer.arm.com/documentation/ddi0403/d/Applicatio.
| ..
| mort96 wrote:
| > In the end, programs will want probably to stay conservative
| and will implement only the core ISA
|
| This is probably not the case. The core ISA doesn't include
| floating point, it doesn't include integer multiply or divide,
| it doesn't include atomic and fence instructions.
|
| What has happened is that most compilers and programs for
| "normal desktop/laptop/server/phone class systems" all have
| some baseline set of extensions. Today, this is more or less
| what we call the "G" extension collection (which is short-hand
| for IMAFD_Zicsr_Zifencei). Though what we consider "baseline"
| in "normal systems" will obviously evolve over time (just like
| how SSE is considered a part of "baseline amd64" these days but
| was once a new and exotic extension).
|
| Then lower power use cases like MCUs will have fewer
| instructions. There will be lots of MCUs without stuff like
| hardware floating point support that won't run binaries
| compiled for the G extension collection. In MCU use cases, you
| typically know at the time of compiling exactly what MCU your
| code will be running on, so passing the right flags to the
| compiler to make sure it generates only the supported
| instructions is not an issue.
|
| And then HPC use cases will probably assume more exotic
| extensions.
|
| And normal "desktop/phone/laptop/server" style use cases will
| have runtime detection of things like vector instructions in
| some situations, just like in aarch64/amd64.
| wren6991 wrote:
| > publish properly what will end up "standard instruction
| fusion patterns" (like the div/rem one).
|
| The div/rem one is odd because I saw it suggested in the ISA
| manual, but I have yet to ever see that pattern crop up in
| compiled code. Usually it's just in library functions like C
| stdlib `div()` which returns a quotient and remainder, but why
| on earth are you calling that library function on a processor
| that has a divide instruction?
| brucehoult wrote:
| > some SiFive cores implement exactly this fusion.
|
| I was not able to open the given link, but it's not true, at
| least for the U74.
|
| Fusion means that one or more instructions are converted to one
| internal instruction (uop).
|
| SiFive's optimisation [1] of a short forward conditional branch
| over exactly one instruction has both instructions executing as
| normal, the branch in pipe A and the other instruction
| simultaneously in pipe B. At the final stage if the branch turns
| out to be taken then it is not in fact physically taken, but is
| instead implemented by suppressing the register write-back of the
| 2nd instruction.
|
| There are only a limited set of instructions that can be the 2nd
| instruction in this optimisation, and loads and stores do not
| qualify. Only simple register-register or register-immediate ALU
| operations are allowed, including `lui` and `auipc` as well as C
| aliases such as `c.mv` and `c.li`
|
| > The whole premise of fusion is predicated on the idea that it
| is valid for a core to transform code similar to the branchy code
| on the left into code similar to the branch-free code on the
| right. I wish to cast doubt on this validity: it is true that the
| two instruction sequences compute the same thing, but details of
| the RISC-V memory consistency model mean that the two sequences
| are very much not equivalent, and therefore a core cannot blindly
| turn one into the other.
|
| The presented code ... mv rd, x0
| beq rs2, x0, skip_next mv rd, rs1 skip_next:
|
| ... vs ... czero.eqz rd, rs1, rs2
|
| ... requires that not only rd != rs2 (as stated) but also that rd
| != rs1. A better implementation is ... mv rd,
| rs1 // safe even if they are the same register bne rs2,
| x0, skip mv rd, x0 skip:
|
| The RISC-V memory consistency model does not come into it,
| because there are no loads or stores.
|
| Then switching to code involving loads and stores is completely
| irrelevant: lw x1, 0(x2) bne x1,
| x0, next next: sw x3, 0(x4)
|
| First of all, this code is completely crazy because the `bne` is
| fancy kind of `nop` and a core could convert it to a canonical
| `nop` (or simply drop it).
|
| Even putting the `sw` between the `bne` and the label is
| ludicrous. There is no branch-free code that does the same thing
| -- not only in RISC-V but also in arm64 or amd64. SiFive's
| optimisation will not trigger with a store in that position.
|
| [1] SiFive materials consistently describe it as an optimisation
| not as fusion e.g. in the description of the chicken bits CSR in
| the U74 core complex manual.
| sxzygz wrote:
| Thanks for your input. I didn't know what to make of the
| article.
| brucehoult wrote:
| Having taken a second look, this article does in fact have a
| point, but it is actually nothing at all to do with
| conditional moves in the RISC-V instruction set Zicond
| extension -- or amd64 or arm64 style conditional moves
| either, if they were added at some point.
|
| It is not even about RISC-V but about instruction fusion in
| general in any ISA with a memory model at least as strong as
| RVWMO -- which includes x86. I'm not as familiar with the
| Aarch64 memory model, but I think this probably also applies
| to it.
|
| The point here is that if an aggressive implementation wants
| to implement instruction fusion that removes conditional
| branches (or indirect branches) to make a branch-free uop --
| for example, to turn a conditional branch over a move into
| something similar to the `czero` instruction -- then in order
| to maintain memory ordering AS SEEN BY A DIFFERENT CORE the
| fused uop has to also have `fence r,w` properties.
|
| That is all.
|
| It is irrelevant to this whether the actual RISC-V
| instruction set has a conditional move instruction, or the
| properties it has if it exists.
|
| It is irrelevant to the situation where a human programmer or
| a compiler might choose to transform branchy code into
| branch-free code. They have a more global view of the program
| and can make sure things make sense. A CPU core implementing
| fusion has only a local view.
|
| Finally, I'll note that instruction fusion is at present
| hypothetical in RISC-V processors that you can buy today
| while it has been used in both x86 and Arm chips for a long
| time.
|
| Intel's "Core" uarch had fusion of e.g. `cmp;bCC` sequences
| in 2006, while AMD added it with Bulldozer in 2011. Arm
| introduced a limited capability -- `CMP r0, #0; BEQ label` is
| given as an example -- in A53 in 2012 and A57, A72 etc
| expanded the generality.
|
| Upcoming RISC-V cores from companies such as Ventana and
| Tenstorrent are believed to implement instruction fusion for
| some cases.
|
| Just for completeness, I'll again repeat that SiFive's U74
| optimises execution of a condition branch and a following
| simple ALU instruction that execute simultaneously in two
| pipelines, but this is NOT fusion into a single uop.
| Dwedit wrote:
| 32-bit ARM had literally every instruction be conditional.
| wren6991 wrote:
| Surprised an article published on September 28, 2025 does not
| include any mention of the P (packed integer SIMD) extension.
|
| P adds instructions like integer multiply-accumulate, which have
| a third register read (for rd). So, they're taking the
| opportunity to add a few forms of 3-register select instructions:
| MVM Move Masked for each bit i: X(rd)[i] =
| X(rs2)[i] ? X(rs1)[i] : X(rd)[i] MVMN Move Masked
| Not for each bit i: X(rd)[i] = X(rs2)[i] ?
| X(rd)[i] : X(rs1)[i] MERGE Merge
| for each bit i: X(rd)[i] = X(rd)[i] ? X(rs2)[i] : X(rs1)[i]
|
| Actually I say I'm surprised but given the way the spec is
| currently spread around different parts of the internet, it's
| easy to miss if you're not following the mailing lists!
| wren6991 wrote:
| I feel like the author might be slightly missing the point here:
|
| > The whole premise of fusion is predicated on the idea that it
| is valid for a core to transform code similar to the branchy code
| on the left into code similar to the branch-free code on the
| right.
|
| The idea of Zicond afaict is that the _compiler_ transforms
| select sequences into (usually multiple) Zicond instructions, and
| cores with more register ports available can fuse Zicond
| compounds into more complex select macro-ops. It 's a 2R1W
| vocabulary for describing selects which require more than 2 read
| ports.
|
| As an aside I evaluated Zicond on my scalar 3-stage
| implementation and found that at 1 CPI for ALU ops and 2-cycle
| taken branch cost, the branchless sequences GCC produced for
| Zicond were never better and sometimes worse than the equivalent
| branching sequence. It really does seem to be targeting bigger
| cores, or constant-time execution
___________________________________________________________________
(page generated 2025-10-02 23:00 UTC)