[RFC] CORE-V (XCV) support for CV32E40P: Clang builtins, XCVsimd intrinsics, and generic auto-selection

Summary

I would like to extend Clang and the RISC-V backend support for the OpenHW Group CORE-V (XCV) vendor extensions implemented by the CV32E40P core. The base XCV instruction definitions are already upstream; this work adds the missing front-end access, SIMD intrinsics, automatic selection of XCV instructions from generic IR, and two correctness fixes.

All XCV extensions are RV32-only (CV32E40P is a 32-bit core).

The complete, integrated work lives on the reference branch xcv_support. Rather than land it as one ~7k-line change, I plan to upstream it as a series of focused PRs (below). This RFC is to align on the overall direction (in particular the generic auto-selection) before the larger PRs land.

What it covers

  • Clang builtins + headers for XCVmac, XCVelw, XCVsimd and XCVbitmanip (builtin defs, CodeGen dispatch, Sema range checks, ClangIR NYI stubs, riscv_corev_*.h headers, CodeGen/Sema tests).
  • XCVsimd LLVM IR intrinsics + SelectionDAG patterns (~150 intrinsics covering the full SIMD ISA, plus a shuffle pseudo expanded in RISCVExpandPseudoInsts).
  • Generic auto-selection of cv.mac/cv.msu (XCVmac), cv.addn/subn/addun/subun + *NR (XCValu), and cv.extractu/extract/bclr/bset/insert (XCVbitmanip) from ordinary C/IR idioms, so common embedded patterns map to CORE-V instructions without builtins. All tied-destination/fused matches are guarded with one-use checks to avoid clobbering values still live in the DAG.
  • Fixes: XCVmem post-increment alignment validation (CV32E40P traps on misaligned half/word access), XCVbi long-branch relaxation for cv.beqimm/cv.bneimm (previously silently truncated), and a missing IsRV32 predicate on the XCVmac block.

Open question for reviewers

The generic auto-selection (emitting vendor cv.* from plain IR rather than from builtins) is the part most worth discussing: is matching these idioms in RISCVISelDAGToDAG / TableGen patterns the desired approach, and are the one-use guards sufficient? Feedback here will shape PR4.

Planned PR series

  1. XCV correctness fixes (XCVmem alignment, XCVbi relaxation, IsRV32) → xcv-fixes.
  2. XCVsimd LLVM intrinsics + patterns → xcv-simd-intrinsics.
  3. Clang CORE-V builtins + headers (depends on #2, as the XCVsimd builtins lower to the intrinsics added in #2; the mac/elw/bitmanip builtins use intrinsics already upstream) → xcv-clang-builtins.
  4. Generic auto-selection of XCVmac/XCValu/XCVbitmanip from generic IR → xcv-generic-select.

With respect to the fixes, I’m not super familiar with the XCV cores but has the misalignment issue always been a problem or are these traps new for the CV32E40P? Either way, the fixes can go in without much before I think.

The IR to XCV instruction selection is also reasonable to add, given assembler support already exists.

Wrt builtins/intrinsics, what we actually add should be things we cannot currently express well in IR right now. For instance you have a lot of add intrinsics - why can’t most of these be an IR add over the corresponding fixed size IR vector type. Please look at how work is proceeding on the P extension (plus intrinsics) to see when we decide we need a new intrinsic/builtin vs when we can use types and operations already available in C/C++/IR. It would also be good if you could point to a spec/documentation for the C level intrinsics (i.e. that are implemented in a header to call a builtin, not the IR intrinsics) if you have one - without this reviews will take longer.

The register allocator will manage this by inserting a copy to preserve the value. There’s no correctness issue. It becomes a code size, performance, register pressure question.

You’re right, it’s not a correctness requirement. I confirmed it: with cv.mac modeled as read-modify-write (Constraints = "$rd = $rd_wb"), reusing the accumulator makes the allocator emit mv a3, a0; cv.mac a3, a1, a2; add a0, a3, a0 and the original value is preserved by a copy, no clobber. The wrong codegen I’d originally seen was from the instruction not being tied; the one-use check was just masking that, and the tie is the actual fix. So I’ll drop the “clobber/miscompile” justification. The only remaining argument for the one-use check is codegen quality: when the inner mul/add has another user, fusing the tied-dst form saves no instruction (the inner op is still materialized) and can add a copy. I’m happy to keep it as an explicit heuristic or drop it and leave it to the allocator/combiner. Do you have a preference?

@lenary:
With respect to the fixes, I’m not super familiar with the XCV cores but has the misalignment issue always been a problem or are these traps new for the CV32E40P?

It’s long-standing: CV32E40P has no misaligned scalar access support, and enableUnalignedScalarMem() is off for the subtarget, so the legalizer already expands misaligned scalar loads/stores to byte accesses before ISel. The branch-relaxation and IsRV32 fixes are independent of that; I’ll address the post-increment one separately (see my reply to @topperc on the PR, I think that hunk is not needed).

@lenary:
why can’t most of these be an IR add over the corresponding fixed size IR vector type?

Agreed. Most XCVsimd ops are element-wise on packed 2 x i16 / 4 x i8 and should be native IR vector ops with ISel patterns, not intrinsics. In the next revision I’ll move add/sub/and/or/xor/shifts/ min/max/abs/avg/neg, the comparisons, and lane insert/extract to v2i16/v4i8 IR, and keep intrinsics only for ops with no faithful IR form (dot product, complex multiply, immediate shuffle/pack, subrotmj, and the saturating/rounding N/RN forms). I’ll also drop the XCVbitmanip builtins: extract/extractu/bclr/bset/insert are exactly what the generic IR-to-XCV selection patch already produces from plain C, so a builtin is redundant (bitrevllvm.bitreverse). Same intrinsic-vs-type line the P-extension work is using.

@lenary:
It would also be good if you could point to a spec/documentation for the C level intrinsics (i.e. that are implemented in a header to call a builtin, not the IR intrinsics) if you have one - without this reviews will take longer.

The instruction semantics are in the CV32E40P user manual: CORE-V Instruction Set Custom Extensions — CORE-V CV32E40P User Manual v1.8.3 documentation I’ll add that reference to the top of each riscv_corev_*.h header. There’s no separate standalone C-intrinsic spec beyond those semantics; the header API is derived from them.

On naming: the builtins use the same __builtin_riscv_cv_<isaext>_<name> scheme as GCC’s CORE-V builtins. I diffed our names against GCC’s gcc/config/riscv/corev.def: the mac and elw builtins match 1:1, and I’ll align the remaining names with GCC for the builtins we keep (e.g. rename cv_alu_sle/cv_alu_sleucv_alu_slet/cv_alu_sletu), so Clang and GCC agree.

Checking intermediate operations will be folded away with hasOneUse is an expected thing to do with that check, so yes go ahead with this.

The xqci vendor extensions have quite a few destination-clobbering instructions, which are mostly unlike existing RVI base instructions. We haven’t added hasOneUse checks to these, but I’m not sure we have a way to revisit isel decisions later when we find out the clobbering instructions need a copy that other instructions might not have. This might be worth investigating as a way to improve codegen quality, rather than for correctness as you originally thought.


Re intrinsics, no docs is not the end of the world, if it’s clear how to go from instruction name to intrinsic name. Exactly how to implement the header defining the (C/C++) intrinsics is up to you (and is closely coupled to your clang implementation), but aligning your builtins spelling with GCC is a good idea. For the P extension we are starting to add cross-project-tests to ensure we can show c/c++ intrinsics translate to the underlying instruction, it might be worth looking at those as an example for what you’re doing. The spelling of e.g. llvm intrinsics is less fundamental.

Thanks.

  • One-use checks: agreed, I’ve reframed them as a codegen-quality heuristic (not correctness) in the commit messages and code comments. And good point on the xqci destination-clobbering instructions. A general way to revisit a fusion when the tied operand turns out to need a copy would be a nice codegen-quality improvement, but I’ll keep that out of this series and just gate on hasOneUse for now.

  • Intrinsics/builtins: I’ve started the rework you suggested, expressing the element-wise XCVsimd ops as native packed-vector IR (2 x i16 / 4 x i8) instead of intrinsics, and removing the now-redundant intrinsics and builtins as it lands. Since that changes which builtins actually exist, I’d rather finish the XCVsimd IR conversion first and then add the cross-project-tests against the final builtin set, instead of writing them now for builtins that are about to go away. I’ll model them on cross-project-tests/intrinsic-header-tests/riscv_packed_simd.c, thanks for the pointer. On spelling, the mac/elw builtins already match GCC’s CORE-V names and I’ll keep the rest aligned.

Following up on the intrinsics-vs-types direction for XCVsimd.

On the XCVsimd PR, @topperc noted that exposing the packed types means committing to the full C vector-type surface (every operation expressible on a __attribute__((vector_size)) type, plus load/store and reinterpret), and asked whether GCC takes the same approach. Combined with the earlier @lenary suggestion to express element-wise ops as native IR rather than dedicated intrinsics, the open question is what the user-facing C API should be: GCC-style builtins, packed vector types, or both.

I tested this against CORE-V GCC (VCS simulation): GCC lets you declare vector types, but it does not map generic vector math to the XCVsimd opcodes. It only exposes XCVsimd through target-specific builtins. So on GCC, vector_size code compiles but is not accelerated by XCVsimd.

Given that, I would propose Clang support both, which is a strict superset of GCC rather than a divergence:

  1. Keep the full __builtin_riscv_cv_* set as the documented C API, matching GCC’s CORE-V builtins, so source written for GCC keeps working unchanged. The element-wise builtins are implemented by emitting the corresponding vector IR (so no dedicated LLVM intrinsic is needed for them); the ops with no IR form (dot product, complex multiply, shuffle, pack, the saturating/rounding forms, and the scalar-broadcast variants) keep their intrinsics.

  2. Additionally lower generic packed-vector IR (2 x i16 / 4 x i8) to the cv.* instructions in the backend. This accelerates vector_size code and autovectorizer output. It does not break GCC compatibility: the same vector code still compiles everywhere, Clang just optimizes it to XCVsimd where GCC would scalarize. Operations with no single instruction are set to Expand, so they scalarize safely (the same effective behavior as GCC).

Net: GCC-compatible builtins as the API, plus IR-vector acceleration as an extra, with a safe scalarization fallback. Does that work for both of you?

@vitbur Good to see this moving forward, thanks for your contribution.

The Open Hardware Foundation has a ratified specification of C builtin functions: core-v-sw/specifications/corev-builtin-spec.md at master · openhwgroup/core-v-sw · GitHub.

You may find it useful to test against upstream GCC/binutils, which has CORE-V assembler and builtin function tests. The reference compiler used for CV32E40P project sign off was an out-of-tree GCC version 14, available here: Tool Chain Downloads – Embecosm . This has a full set of tests of the assembler and C builtins for all 8 CORE-V ISA extensions.