[RISC-V] NativeAOT soft-float target (riscv64-lp64) and builds without the A extension - #134456
maximmenshikov wants to merge 16 commits into
Conversation
|
Azure Pipelines: Successfully started running 7 pipeline(s). 9 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging @dotnet/jit-contrib for JIT-EE GUID update |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
5f0c933 to
2814988
Compare
3231ec6 to
384a139
Compare
riscv64 already models its optional extensions - Zba, Zbb, Zbs and Zicond - as InstructionSets, but the base rv64gc extensions are assumed unconditionally, so there is no way to describe a target that does not have them and no way for the JIT to ask. Add F, D, C and A to InstructionSetDesc.txt with D implying F, seed the rv64gc baseline from them in the VM and in the AOT drivers, and give them the usual DOTNET_EnableRiscV64* opt-outs, the same shape the other architectures use for their optional sets. The public names are the psABI extension letters, so a reduced target is spelled the way the C toolchain spells it: --instruction-set=-a,-c,-d,-f. The R2R names are spelled RiscV64F/D/C/A. The JIT and managed enums scope their members (InstructionSet.RiscV64_F), but the R2R one does not, and it is the only one of the three that is a format contract and cannot be renamed afterwards, so a bare READYTORUN_INSTRUCTION_A would be a poor permanent name. Defaults are unchanged: every riscv64 target still gets F, D, C and A unless something explicitly removes them, so this is a no-op for the supported rv64gc baseline. The JIT/EE interface GUID is updated, as instruction set definitions are part of that contract. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
With F, D, C and A described as instruction sets, let the riscv64 backend ask before emitting an encoding that needs one: * tryEmitCompressedIns_R_R_R - the only place the JIT emits a compressed encoding - returns false unless C is available. * A Checked JIT asserts in emitOutput_Instr that no F/D-extension major opcode is emitted unless F is available, so an unexpanded floating point node fails on the method that produced it rather than at run time on the target. The assert needs compIsaSupportedDebugOnly, which was a no-op returning false outside xarch and arm64; enable the real check for riscv64 too. Both are no-ops on the rv64gc baseline, where F, D, C and A are always present. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The native runtime is compiled -march=rv64gc -mabi=lp64d unconditionally, so a
riscv64 target whose sysroot is built for another ABI cannot be built at all:
the objects claim lp64d, the sysroot CRT claims something else, and lld refuses
to reconcile the two. The only way out today is to append overriding flags after
these, which works by accident of ordering and hides what the build targets.
Turn the two into cache variables with the current values as defaults, so an
unconfigured build is byte-identical, and a target that needs a different ABI
states it once:
cmake -DCLR_CMAKE_RISCV64_MABI=lp64 -DCLR_CMAKE_RISCV64_MARCH=rv64im ...
The float-ABI field of e_flags follows -mabi rather than -march, so setting the
ABI here is what makes the whole native build agree with its sysroot. The flags
are passed at link time as well, so the driver selects the matching CRT and
builtins.
This mirrors how armel selects -mfloat-abi=softfp a few lines above.
Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
configurecompiler.cmake settles the RISC-V ISA and ABI for the project, but cmake compiles and links its own probes at project() time, before any of the project's options exist. On a sysroot built for an ABI other than the compiler default those probes fail to link - lld does not reconcile float ABIs - and cmake reports the compiler as broken before the build starts. Apply the same two variables from the cross toolchain file, which is in effect for the probes as well, and list them in CMAKE_TRY_COMPILE_PLATFORM_VARIABLES so they survive nested try_compile passes. Both are unset by default, so the toolchain's own defaults stay in place and nothing changes for existing targets. This follows what the arm/armel branch above already does for -mfpu and -mfloat-abi. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The riscv64 port refuses to compile when __riscv_flen is not 32 or 64, so a
target built for the base integer ISA cannot build the PAL's unwinder at all:
libunwind-riscv.h has no unw_tdep_fpreg_t, asm.h has no load/store width, and
dwarf_getfp/dwarf_putfp hit an #error because __riscv_xlen != __riscv_flen.
There is nothing to unwind in that configuration - the target has no
floating-point registers - so the three cases are answered rather than made to
work:
* unw_tdep_fpreg_t is part of the public API, so it stays, as an integer of
the width the double-precision ABI uses.
* asm.h leaves SZFREG/STORE_FP/LOAD_FP undefined; getcontext.S and
setcontext.S already guard every use with #ifdef, so they save and restore
the integer registers alone.
* dwarf_getfp and dwarf_putfp return -UNW_EBADREG: with no floating-point
registers, no floating-point location is valid.
Targets that have F or D are unaffected - the existing branches are untouched
and still the only ones taken.
Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The hand-written riscv64 assembly uses fld/fsd and the A-extension amo* unconditionally, so a runtime built for a target without F/D or A does not assemble at all - the failure is in the assembler, before any of the ISA modelling in the compiler can help. Guard those sequences on the __riscv_flen and __riscv_atomic predefined macros, which are exactly what -march sets, and provide the lr/sc-free fallback for the atomics. A build that has the extensions is unchanged: the guards are true and the same instructions are emitted. The .S sources are the one place where the instruction set cannot be a compiler concept - the assembler is the consumer - so they switch on the predefined macros rather than on the InstructionSet model used elsewhere. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
cpufeatures.c asserts the full rv64gc baseline at startup, so a runtime built for a narrower ISA fails the assert on hardware that matches what it was built for. Assert what the build actually requires instead. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Under CORJIT_FLAG_SOFTFP_ABI - the flag armel already uses - riscv64 passes and returns floating-point scalars and floating-point struct fields by the integer calling convention, which is what the lp64 psABI prescribes for a target without F/D. Only the ABI classifier changes here; code generation still uses the FP registers, so this commit alone is not a usable target. It is separated because it is the part that is purely a calling convention. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Fourteen helpers - {FLT,DBL}{ADD,SUB,MUL,DIV}, {FLT,DBL}CMP_{LE,GE}, FLT2DBL and
DBL2FLT - bound by ilc to the toolchain's compiler-rt builtins (__adddf3,
__ledf2, ...), the same way CORINFO_HELP_DBLREM is already bound to fmod. No
floating-point arithmetic is implemented in the runtime or the libraries.
The target is selected explicitly as TargetAbi.NativeAotRiscV64SoftFloat,
spelled riscv64-lp64 on the ilc command line, following the armel precedent. It
drives the JIT flag, the RISC-V ELF float-ABI field in e_flags (which is what
lets the linker reject a mix of lp64 and lp64d objects), the instruction-set
defaults and a two-way validation: lp64d requires F and D, lp64 must not have
them. crossgen2 rejects the target - the helpers have no ReadyToRun encoding.
Value numbering models the helpers as the operations they implement, as
CORINFO_HELP_LMUL is modelled on 32-bit targets; only the two three-way
compares need VNFuncs of their own.
Note: the RiscV64ObjectWriterTests addition was dropped - ILCompiler.Compiler.Tests
was deleted upstream in dotnet#133474 and needs a new home.
Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The IR keeps TYP_FLOAT and TYP_DOUBLE; what changes under the soft-float ABI is their register class, which becomes VTR_INT. Most of the backend already decides by register class, so FP values are then allocated, spilled, moved, passed and returned in integer registers with no further work; the places that decide by varTypeIsFloating instead are adjusted. The operations are expanded in global morph through the existing hooks: arithmetic and conversions into helper calls, comparisons into a compare helper plus an integer relop, negation into a sign-bit XOR, and CKFINITE into a bounds check on the exponent field. FP Math intrinsics are declined by type and remain calls to the managed implementations. The debug JIT asserts on any F/D opcode reaching the emitter when F is not in the instruction set, so an unexpanded node fails on the method that produced it. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Bit-exact expectations for arithmetic rounding, NaN comparison semantics including the unordered branch forms, saturating and checked conversions for every integer width, conversions from every integer width, float <-> double, negation, remainder, the Math intrinsics that become calls, CSE of repeated helper calls, values across call boundaries including stack-passed arguments, and float-bits-as-int sign extension. Every expected value is the exact IEEE 754 result, so the test is valid on every target and passes on hard-float today; on a soft-float image the same assertions exercise the compiler-rt helpers. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
The ILCompiler is published as a NativeAOT application for the target, and it
gets --targetarch from _targetArchitectureWithAbi. On a riscv64 target that
value is plain "riscv64", so the object writer stamps
EF_RISCV_FLOAT_ABI_DOUBLE; against a soft-float sysroot the link then fails:
ld.lld: error: ilc.o: cannot link object files with different
floating-point ABI from Scrt1.o
Add the riscv64 case next to the armel one that is already there. It is
conditional on a property rather than derived from the RID, because
linux-musl-riscv64 covers both lp64d and lp64 userspaces - unlike linux-bionic
on ARM, where armel follows from the RID alone.
Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
…or riscv64-lp64
The aggregate-executable shim is compiled from C and linked by clang straight from
MSBuild; nothing there knows the target ABI, so on a soft-float sysroot the shim
object comes out lp64d and lld refuses to link it against Scrt1.o:
ld.lld: error: /tmp/dotnet-dev-certs-b44bf8.o: cannot link object files
with different floating-point ABI from /crossrootfs/riscv64/usr/lib/Scrt1.o
Derive the flag from _targetArchitectureWithAbi, the same property that already
selects --targetarch for ilc, and mark it Shim="true" so both the shim and the
main link get it.
Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
genLockedInstructions and genCodeForCmpXchg emit amo* and lr/sc unconditionally. A build that selects an ISA without the A extension gets instructions the target cannot execute, on any Interlocked call. Gate them on InstructionSet_A. Without it, lower to a plain read/modify/write and, for CmpXchg, to a compare and store with no reservation pair. This is the only lowering the ISA allows, and it is correct on a target with a single hart and no preemption. BuildNode in lsrariscv64 extends the address and data lifetimes for the plain sequence, which reuses both registers after the first instruction, and gives the arithmetic and bitwise forms one scratch register for the new value. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
…/EE GUID The A-extension test was a substring match for "a" against the whole -march string, which is wrong in both directions. rv64gc has the extension, through the "g" shorthand for imafd, but contains no letter "a". rv64im_zba does not have it, but contains "a" in the name of Zba. So the default baseline linked libatomic it does not need, and a build that really lacks A would fail on the atomic-alignment warning under -Werror. Match on the single-letter part of the string only, after stripping the rv<XLEN> prefix and any multi-letter extensions, and treat "g" as implying A. Also generate a new JITEEVersionIdentifier: the new CorInfoHelpFunc entries shift the values of every helper after them, so a JIT and an EE from different sides of this change must not be considered compatible. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
Without A there is no atomic memory operation to emit, so Interlocked and CmpXchg lower to a plain read/modify/write. Whether that is sufficient depends on the target running on one hart and never being preempted. Neither the ISA nor the ABI says anything about that: a soft-float lp64 image is perfectly normal on rv64imac hardware with threads, and would lose atomicity silently. So it has to be asserted rather than inferred. Add --single-hart to ilc and reject an ISA without A unless it is passed. The flag names the property it claims, and the failure is at compile time rather than at run time. Signed-off-by: Maxim Menshikov <maksim.menshikov@nethermind.io>
384a139 to
b78b9cd
Compare
|
Since this is out of draft: the goal reads clearly (a soft-float One naming concern: |
Please don't review this yet. I opened it to run CI and to show the full series. The design will be discussed first in a separate design issue, which I'm about to file; I'll link it here. I'll keep this PR as a draft until the direction is agreed there, and then split it into smaller PRs.
Summary:
riscv64-lp64NativeAOT target: lp64 calling convention, FP arithmetic via compiler-rt helpers, correct ELFe_flags. crossgen2 rejects the target.Interlocked/CmpXchglowering without the A extension, gated behind ilc's--single-hart.Context: #132204.