Repository navigation
Universal x64 JIT for crossgen2/ILC #52
Description
Universal x64 JIT for crossgen2/ILC — Strategy
Goal
Replace the two cross-targeting x64 JITs shipped with crossgen2/ILC
(clrjit_unix_x64_<host> and clrjit_win_x64_<host>) with a single
clrjit_universal_x64_<host> that decides OS-dependent behavior at runtime.
The in-proc clrjit (runtime) build must stay compile-time specialized (no
runtime OS branches), exactly like arm/arm64 today.
How arm/arm64 already do this (the template)
src/coreclr/inc/targetosarch.h:TargetOS::IsWindows/IsUnix/IsApplePlatform
are compile-timeconst boolwhenTARGET_WINDOWS/TARGET_UNIXis defined,
but become runtimestatic boolwhen neither is defined
(TARGET_OS_RUNTIMEDETERMINED). The "universal" cmake OS sets neither.jit/target.h#undefs TARGET_UNIX/WINDOWS/OSX/APPLE so JIT code cannot use
them directly; it must useTargetOS::*.CILJit::setTargetOS(ee_il_dll.cpp) sets the runtime values; VM/crossgen call
it before compiling.- ABI knobs that vary (arm hardfp/softfp) use
CONFIGURABLE_ARM_ABI:
a compile-timeconst bool compFeatureHfabecomes a runtime variable. - Runtime-variable register masks already exist for APX/EVEX:
rbmAllInt,
rbmIntCalleeTrash,rbmAllFloat, ... are instance fields set in
compiler.cppinit fromRBM_*_INIT+ runtime capability checks.
RBM_ALLINT/etc. are macros that callget_RBM_*().
The x64-specific gap
TargetOS is already runtime-capable; the problem is x64 ABI is selected by the
compile-time macro UNIX_AMD64_ABI (and its negation WINDOWS_AMD64_ABI),
which drives, in targetamd64.h and jit.h:
- Register sets / masks:
RBM_INT_CALLEE_SAVED,RBM_INT_CALLEE_TRASH_INIT,
RBM_FLT_CALLEE_SAVED,RBM_FLT_CALLEE_TRASH_INIT, arg regs (REG_ARG_0…),
REG_VAR_ORDER_*, callee-save countsCNT_CALLEE_*,LAST_FP_ARGREG. - Feature constants:
FEATURE_MULTIREG_ARGS/RET/…(0 on win, 1 on unix),
MULTIREG_HAS_SECOND_GC_RET,FEATURE_PARTIAL_SIMD_CALLEE_SAVE,
FEATURE_IMPLICIT_BYREFSsizing,MAX_*_MULTIREG_BYTES,MAX_RET_REG_COUNT,
ETW_EBP_FRAMED,MIN_ARG_AREA_FOR_CALL, profiler arg regs. - Struct classification (SysV eightbyte classifier) — Unix-only code.
Scale (jit/ tree): UNIX_AMD64_ABI 184 refs / 81 #if sites; WINDOWS_AMD64_ABI 28;
FEATURE_MULTIREG_RET 78; FEATURE_PARTIAL_SIMD_CALLEE_SAVE 75;
MULTIREG_HAS_SECOND_GC_RET 29; ETW_EBP_FRAMED 29.
The hard part: several of these feed compile-time-constant array sizes and enum
values (e.g. MAX_RET_REG_COUNT, MULTIREG_HAS_SECOND_GC_RET), so they can't
become plain runtime variables without either (a) sizing arrays for the max
(union of both ABIs) or (b) keeping the constant at the max and bounding at
runtime.
Proposed strategy (staged)
Stage 0 — Build scaffolding (no behavior change)
- Add
CONFIGURABLE_AMD64_ABIcompile define, set only on a new
clrjit_universal_x64_<host>target injit/CMakeLists.txt. - Add cmake OS
universal+x64handling inclrdefinitions.cmake
(set_target_definitions_to_custom_os_and_arch): define TARGET_AMD64/64BIT but
neither TARGET_UNIX nor TARGET_WINDOWS nor UNIX_AMD64_ABI → forces
TARGET_OS_RUNTIMEDETERMINED and the configurable ABI path. - Keep the existing
clrjit_unix_x64/clrjit_win_x64targets building until
the universal one is proven, then delete them and flip
JitConfigProvider.GetTargetSpecto emituniversal_x64.
Stage 1 — Make ABI a runtime property
- Introduce a single runtime predicate
compUnixAmd64Abi()/
Compiler-level (or global, since immutable per-process) boolean initialized
fromTargetOS::IsUnixinsetTargetOS, mirroringCONFIGURABLE_ARM_ABI's
compFeatureHfa. Under the non-configurable native build it is a
const boolderived fromUNIX_AMD64_ABI.
Stage 2 — Convert register/mask definitions to runtime
- Extend the existing
rbm*/cnt*init block incompiler.cppto select the
Unix vs Windows base sets bycompUnixAmd64Abi()(union feeds APX/EVEX ORs as
today). ConvertRBM_INT_CALLEE_SAVED,RBM_FLT_CALLEE_SAVED,
RBM_FLT_CALLEE_TRASH_INIT,REG_ARG_*,REG_VAR_ORDER_*,CNT_CALLEE_*,
LAST_FP_ARGREGtoget_*()accessors backed by instance fields (native build
keeps them asconst/#definevia the non-configurable branch).
Does this ripple into other architectures? (No, with one bounded caveat.)
- RBM masks are defined per-arch in
targetXXX.h; shared code (lsra,
compiler, codegencommon) only references the macro name. Whether
RBM_INT_CALLEE_SAVEDexpands to a constant orget_RBM_INT_CALLEE_SAVED()is
transparent to consumers, so converting x64 is done entirely in
targetamd64.h+compiler.{h,cpp}. arm64/arm/x86/loong/riscv headers are
untouched and keep plain-constant#defines. - Proven precedent:
targetamd64.halready definesRBM_ALLINT,
RBM_ALLFLOAT,RBM_INT_CALLEE_TRASH,RBM_FLT_CALLEE_TRASHasget_*()
accessors (APX/EVEX) while every other arch keeps them constant — that
conversion touched zero other architectures. This work converts the remaining
x64 masks (RBM_INT_CALLEE_SAVED,RBM_FLT_CALLEE_SAVED, arg/order/count) the
same way. - Caveat:
get_*()is aCompilermember and cannot be used in a compile-time
constant context (file-scopestatic const,constexpr, array size). If a
shared consumer uses a newly-runtime mask that way, that site is reworked
to compute at runtime. This edits shared code but stays behavior-identical and
constant-folds on other arches (their macros remain constants) — no
retargeting or per-arch behavior change. The APX/EVEX conversion already flushed
out these sites forRBM_ALLINT/RBM_INT_CALLEE_TRASH, so the remaining set is
bounded and analogous.
Stage 3 — Convert feature flags & sizing
- For booleans (
FEATURE_MULTIREG_*,MULTIREG_HAS_SECOND_GC_RET,
FEATURE_PARTIAL_SIMD_CALLEE_SAVE,ETW_EBP_FRAMED,
FEATURE_MULTIREG_STRUCT_PROMOTE): under CONFIGURABLE_AMD64_ABI make them
runtime predicates; native build keeps#defineconstants. - For array-sizing constants (
MAX_RET_REG_COUNT,MAX_*_MULTIREG_BYTES,
MAX_ARG_REG_COUNT,MAX_MULTIREG_COUNT): in the configurable build fix them
to the union max (Unix values) and rely on runtime feature predicates to
gate behavior; verify no correctness dependence on the smaller Windows values.
Stage 4 — Guard remaining #ifdef UNIX_AMD64_ABI code
- Convert each of the 81
#ifsites: pure-codegen branches →if (compUnixAmd64Abi()); Unix-only helpers (SysV classifier) compiled
unconditionally in the configurable build and called only on the Unix path. - Windows-only (varargs shadow,
WINDOWS_AMD64_ABI) likewise runtime-gated.
Stage 5 — Validation
- Diff crossgen2 output (R2R images) for a representative assembly built with the
old per-OS JITs vs the universal JIT for both target OSes — require identical
codegen (SuperPMI replay is the ideal gate: collect on both OS targets, replay
with the universal JIT, expect zero diffs). - Build both the native
clrjit(compile-time) andclrjit_universal_x64
(runtime) to confirm the dual-mode macros compile both ways.
Key files
src/coreclr/jit/CMakeLists.txt(targets, defines)src/coreclr/clrdefinitions.cmake(set_target_definitions_to_custom_os_and_arch)src/coreclr/inc/targetosarch.h(already runtime-ready)src/coreclr/jit/target.h,targetamd64.h,targetamd64.cpp,jit.hsrc/coreclr/jit/compiler.cpp/.h(rbm/cnt init + accessors)src/coreclr/jit/ee_il_dll.cpp(setTargetOS)src/coreclr/tools/Common/JitInterface/JitConfigProvider.cs(GetTargetSpec)
GCInfo encoder (linked into the JIT binary)
The JIT links a per-(os,arch) gcinfo static lib: gcinfo_unix_x64 /
gcinfo_win_x64 (arm64 uses gcinfo_universal_arm64). Built by
create_gcinfo_lib in src/coreclr/gcinfo/CMakeLists.txt, which calls
set_target_definitions_to_custom_os_and_arch — so the unix lib is compiled with
TARGET_UNIX+UNIX_AMD64_ABI, the win lib with TARGET_WINDOWS.
Findings from auditing the encoder for x64 OS-dependence
inc/gcinfotypes.hAMD64GcInfoEncoding: all constants (normalization,
ENCBASE,HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA, stack-base-reg encode) are
OS-independent — identical for Unix and Windows x64.gcinfo/gcinfoencoder.cpp,arraylist.cpp,simplerhash.cpp,
inc/gcinfoencoder.h,inc/gcinfo.h,gcinfohelpers.h: zero
UNIX_AMD64_ABI/WINDOWS_AMD64_ABI/TARGET_UNIX/TARGET_WINDOWSuses. The
only OS branch is one runtime check (TargetOS::IsApplePlatform && TargetArchitecture::IsArm64) at gcinfoencoder.cpp:573 — already runtime,
arm64-only, compile-time-false for x64.- JIT-side GC logic (
jit/gcencode.cpp,jit/gcinfo.cpp,jit/jitgcinfo.h):
zero OS/ABI conditionals; register numbers/EnC area are passed through from
JIT computations (which live in the JIT proper, covered by the main strategy).
Consequence / required work
The x64 gcinfo encoder is effectively already OS-agnostic — the two libs
differ only by target #defines the gcinfo code never consumes, so they are
functionally identical object code. Unlike the JIT proper, there is little/no
#ifdef conversion to do here. The concrete work is:
- Build wiring: add
create_gcinfo_lib(TARGET gcinfo_universal_x64 OS universal ARCH x64)and link it intoclrjit_universal_x64(the
unix/winx64 gcinfo libs can be retired with the corresponding JITs).
OS universal ARCH x64correctly yields TARGET_AMD64/64BIT with neither
TARGET_UNIX nor TARGET_WINDOWS ⇒targetosarch.hruntime-determined TargetOS,
and noUNIX_AMD64_ABI. - Guardrail: enforce that the encoder never introduces compile-time OS
macros for x64 — any future OS-dependent behavior must use runtime
TargetOS::*(as the arm64-apple assert already does), so the single
universal lib stays correct for both target OSes. - Validation: binary-diff
gcinfo_unix_x64/gcinfo_win_x64today to
confirm they are identical (sanity), then confirmgcinfo_universal_x64
links and R2R output is bit-identical vs the per-OS libs via SuperPMI replay.
Net: the gcinfo effort is small relative to the JIT proper, but it IS required —
we must produce and link a runtime-determined (universal) gcinfo build rather
than a per-OS one, exactly the same "swap to runtime TargetOS" model, and keep
the encoder free of compile-time x64 OS macros going forward.
Risk / cost
Large: ~180 macro refs, register enums, array sizing. Must not regress native
JIT codegen or perf. Best landed incrementally behind the existing per-OS JITs.
Note
This issue's content was generated with GitHub Copilot CLI assistance.
Activity
davidwrighton commentedon Jul 23, 2026
Mechanical implementation recipe — unified
constexprternaryThis is the concrete, low-risk recipe for converting the two cross-targeting x64 JITs into a single universal (
CONFIGURABLE_AMD64_ABI) build. It supersedes the "three-way#ifdef" sketch: instead of duplicating each macro three ways, use one unified expression per OS-dependent value.Core idea
Make
compUnixAmd64Abi()(jit/target.h)constexprexcept underTARGET_OS_RUNTIMEDETERMINED(the universal build), where it stays a plaininlineruntime query:Then each OS-dependent register value becomes a single expression that replaces its
#ifdef UNIX_AMD64_ABI / #elseblock:TargetOS::IsUnixis aconst bool, so the ternary constant-folds → identical JIT output (SuperPMI zero-diff holds even in unoptimized builds because the value is identical) and it is usable instatic_assert/array-size/constant contexts.thisis needed (it reads theTargetOS::IsUnixstatic bool), so it works in free functions, NSDMIs (regset.h), and the typelist runtime table build — unlike aCompiler::get_*()accessor.Classification of the OS-dependent macros
1. Runtime register VALUES → unified ternary (replace the
#ifdef UNIX_AMD64_ABI/#elseblocks intargetamd64.h):MIN_ARG_AREA_FOR_CALL,RBM_INT_CALLEE_SAVED,RBM_INT_CALLEE_TRASH_INIT,RBM_FLT_CALLEE_SAVED,RBM_FLT_CALLEE_TRASH_INIT,REG_EXCEPTION_OBJECT/RBM_EXCEPTION_OBJECT,RBM_ENC_CALLEE_SAVED,LAST_FP_ARGREG,REG_ARG_0..5/RBM_ARG_0..5/REG_ARG_FIRST/INIT_ARG_STACK_SLOT,RBM_ARG_REGS,RBM_FLTARG_REGS,REG_FLTARG_4..7/RBM_FLTARG_4..7,RBM_STACK_PROBE_HELPER_TRASH,RBM_STOP_FOR_GC_TRASH,RBM_PROFILER_ENTER_TRASH/LEAVE_TRASH/TAILCALL_TRASH, and theCNT_*counts (CNT_CALLEE_SAVED,CNT_CALLEE_SAVED_FLOAT,CNT_CALLEE_TRASH_INT_INIT,CNT_CALLEE_TRASH_FLOAT_INIT).RBM_INT_CALLEE_SAVEDexcludes EBP (SysV frames it:RBM_EBX|RBM_R12|RBM_R13|RBM_R14|RBM_R15); Windows arm includesRBM_EBP(RBM_EBX|RBM_ESI|RBM_EDI|RBM_EBP|RBM_R12|RBM_R13|RBM_R14|RBM_R15). Do not depend on theRBM_ETW_FRAMED_EBPmacro.2. Compile-time FEATURE flags & array sizes → universal takes the Unix SUPERSET. Widen guards to
#if defined(UNIX_AMD64_ABI) || defined(CONFIGURABLE_AMD64_ABI)so universal gets:FEATURE_IMPLICIT_BYREFS=1,FEATURE_MULTIREG_ARGS_OR_RET=1,FEATURE_MULTIREG_ARGS=1,FEATURE_MULTIREG_RET=1,FEATURE_MULTIREG_STRUCT_PROMOTE=1,MAX_PASS_MULTIREG_BYTES=32,MAX_RET_MULTIREG_BYTES=32,MAX_ARG_REG_COUNT=2,MAX_RET_REG_COUNT=4,MAX_MULTIREG_COUNT=4,MAX_REG_ARG=6,MAX_FLOAT_REG_ARG=8.FEATURE_PARTIAL_SIMD_CALLEE_SAVEis already1in universal via its#else. Windows behavior is then obtained at runtime by the ternary register values (e.g.RBM_FLT_CALLEE_SAVEDempty on unix) and by restricting the usable arg-reg count in the classifier. TheintArgRegs/fltArgRegsextern constarray declarations are sized to the unix (6/8) values in universal; keep only one declaration reachable.REG_INTRET_1/REG_FLOATRET_1(unix-only today) get a#elif defined(CONFIGURABLE_AMD64_ABI)arm so multireg-return code compiles; they only execute at runtime on the SysV path.REG_PROFILER_ENTER_ARG_0/1,RBM_,REG_DEFAULT_PROFILER_CALL_TARGET), currently unix-only, must be available in universal — widen their guard.3.
ETW_EBP_FRAMED(unix 1 / win 0) → runtime in universal. Forcing1on Windows breaks zero-diff;0on Unix is incorrect. ForCONFIGURABLE_AMD64_ABIdefine#define ETW_EBP_FRAMED (compUnixAmd64Abi() ? 1 : 0)(native keeps the plain#ifdef1/0). Convert every amd64-compiled#if ETW_EBP_FRAMEDsite to a runtimeif (ETW_EBP_FRAMED)with both branches compiling:codegenxarch.cpp(×3),lsra.cpp,regalloc.cpp,codegencommon.cpp,target.h,unwindamd64.cpp,unwind.cpp. Leave the#ifin arch files never compiled for amd64 (codegenarmarch.cpp,codegenloongarch64.cpp,codegenriscv64.cpp).4. Register enumeration order (
REG_VAR_ORDER/_FLT) differs by OS. Consumers are file-scopestatic const regNumber lsraRegOrder[] = {REG_VAR_ORDER};etc. inlsrabuild.cpp(~1886). UnderCONFIGURABLE_AMD64_ABIonly, provide two static arrays (unix + windows order) selected at runtime bycompUnixAmd64Abi(); native keeps its single unchanged array. Same forlsra.cppLsraLimitSmallIntSet(DEBUG, file-scopestatic const regMaskTP) → make runtime/function-local in universal.5. Arg-reg arrays + ABI classifier (
targetamd64.cpp,abi.h):targetamd64.cppdefinesintArgRegs/intArgMasks/fltArgRegs/fltArgMaskstwice (unix 6/8, win 4/4). For universal, define the union-max (unix 6/8) arrays and compile bothSysVX64ClassifierandWinX64Classifierbodies plusGetShadowSpaceCallerOffsetForReg(currently windows-only).WinX64Classifiermust build its register queues with the windows count (4), notArrLenof the now-size-6 array;SysVX64Classifieruses all 6/8.abi.h:PlatformClassifieris chosen by#elif defined(WINDOWS_AMD64_ABI)/UNIX_AMD64_ABI. For universal add a runtime dispatcher (e.g.UniversalX64Classifier) that, from itsconst ClassifierInfo&ctor, selects SysV vs Win bycompUnixAmd64Abi()and forwardsClassify(Compiler*, var_types, ClassLayout*, WellKnownArg)/StackSize();typedef UniversalX64Classifier PlatformClassifier;. Match the interface used atlclvars.cpp:1083,morph.cpp:1837/1933, and theSwiftABIClassifierwrapper.6. True compile-time-constant consumers to special-case:
target.hstatic_assert(...RBM_INT_CALLEE_SAVED...)(no overlap with SPBASE/FPBASE) → guard#ifndef CONFIGURABLE_AMD64_ABI(property holds for both OS; the mask is not a constant expression in the universal build).Constraints / gates
build.cmd clr.alljits -c Checked(targetclrjit_universal_x64_<host>); nativebuild.cmd clr.jit -c Checkedstays green.UNIX_AMD64_ABI~184 refs / 81#ifsites;FEATURE_MULTIREG_RET78;FEATURE_PARTIAL_SIMD_CALLEE_SAVE75;MULTIREG_HAS_SECOND_GC_RET29;ETW_EBP_FRAMED29.Note
This comment was generated by GitHub Copilot (AI).