From 4c1fbd39835e3fbd3a66cb1ac03efaca701f9f2d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 18 Jun 2026 16:04:44 -0500 Subject: [PATCH 1/2] [mono][llvm] Recognize MathF.Abs/Log and Single/Double.MinNumber/MaxNumber as intrinsics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mono's LLVM backend has end-to-end plumbing for OP_FMA, OP_FCOPYSIGN, OP_SQRTF, etc., but a handful of obvious BCL entry points fall through to the C# implementations: * `MathF.Abs(float)` and `MathF.Log(float)` are not recognized in the MathF block of intrinsics.c, even though MathF.Sqrt/Sin/Cos/Exp/Log2/ Log10/Floor/Ceiling/Truncate all are. (Math.Abs(float) is recognized via the Math (float) fallback, but Math.Log(float) doesn't exist — only MathF.Log does.) Adding INTRINS_LOGF + OP_LOGF closes the gap; MathF.Abs reuses the existing OP_ABSF / INTRINS_ABSF. * `Single.MinNumber/MaxNumber` and `Double.MinNumber/MaxNumber` (forwarders for INumber.{Min,Max}Number) are IEEE 754-2008 numNum semantics: when exactly one argument is NaN, return the non-NaN; when both are NaN, return NaN. These map exactly to llvm.minnum / llvm.maxnum, which on AArch64 lower to a single fminnm/fmaxnm instruction. Without recognition the C# bodies inline to a fcmp+select chain — semantically correct, but several instructions on every target. Mono had no recognition for the Single/Double primitive classes previously; this adds a focused block that only handles MinNumber and MaxNumber. (Min/Max forward to Math/MathF and are caught by the existing recognition there.) Adds: * OP_LOGF / OP_FMINNUM / OP_FMAXNUM / OP_RMINNUM / OP_RMAXNUM in mini-ops.h. * INTRINS_LOGF / INTRINS_MINNUM / INTRINS_MINNUMF / INTRINS_MAXNUM / INTRINS_MAXNUMF in llvm-intrinsics.h. * Case handlers in mini-llvm.c (OP_LOGF reuses the existing scalar-1-arg pattern; the four num/numF ops share a single block that selects the correct intrinsic by opcode). * Recognition in intrinsics.c — MathF.Abs/MathF.Log added to the existing MathF (float) block; MinNumber/MaxNumber added in a new Single/Double block placed after the Math block. Validation: on the PR #129299 branch (emsdk 5.0.6 / LLVM 23) stacked with the Min/Max NaN fix from PR #129593, full AOT + LLVM run of System.Runtime.Tests on iossimulator-arm64: HalfTests 1442/1442 pass HalfTests_GenericMath 356/356 pass SingleTests 1334/1334 pass DoubleTests 1559/1559 pass (no regression vs the #129593-only baseline; covers MinNumber/ MaxNumber on Single/Double) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/mono/mini/intrinsics.c | 32 +++++++++++++++++++++++++++- src/mono/mono/mini/llvm-intrinsics.h | 13 +++++++++++ src/mono/mono/mini/mini-llvm.c | 32 ++++++++++++++++++++++++++++ src/mono/mono/mini/mini-ops.h | 5 +++++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/mono/mono/mini/intrinsics.c b/src/mono/mono/mini/intrinsics.c index 4b8c9020084783..d30c5e67c4045d 100644 --- a/src/mono/mono/mini/intrinsics.c +++ b/src/mono/mono/mini/intrinsics.c @@ -102,7 +102,9 @@ llvm_emit_inst_for_method (MonoCompile *cfg, MonoMethod *cmethod, MonoMethodSign if (in_corlib && !strcmp (m_class_get_name (cmethod->klass), "MathF") && cfg->r4fp) { // (float) if (fsig->param_count == 1 && fsig->params [0]->type == MONO_TYPE_R4) { - if (!strcmp (cmethod->name, "Ceiling")) { + if (!strcmp (cmethod->name, "Abs")) { + opcode = OP_ABSF; + } else if (!strcmp (cmethod->name, "Ceiling")) { opcode = OP_CEILF; } else if (!strcmp (cmethod->name, "Cos")) { opcode = OP_COSF; @@ -110,6 +112,8 @@ llvm_emit_inst_for_method (MonoCompile *cfg, MonoMethod *cmethod, MonoMethodSign opcode = OP_EXPF; } else if (!strcmp (cmethod->name, "Floor")) { opcode = OP_FLOORF; + } else if (!strcmp (cmethod->name, "Log")) { + opcode = OP_LOGF; } else if (!strcmp (cmethod->name, "Log2")) { opcode = OP_LOG2F; } else if (!strcmp (cmethod->name, "Log10")) { @@ -285,6 +289,32 @@ llvm_emit_inst_for_method (MonoCompile *cfg, MonoMethod *cmethod, MonoMethodSign } } + if (in_corlib && (!strcmp (m_class_get_name (cmethod->klass), "Single") || !strcmp (m_class_get_name (cmethod->klass), "Double"))) { + // Recognize the IEEE 754-2008 numNum/maxNum helpers on Single/Double. These + // live on the primitive types (forwarded from INumber), not on + // Math/MathF. Lower them to llvm.minnum/llvm.maxnum (NaN-suppressing), + // which matches the spec ("if either is NaN, return the non-NaN; if both + // are NaN, return NaN") and on AArch64 maps to a single fminnm/fmaxnm. + opcode = 0; + if (fsig->param_count == 2 && + fsig->params [0]->type == fsig->params [1]->type && + (fsig->params [0]->type == MONO_TYPE_R4 || fsig->params [0]->type == MONO_TYPE_R8)) { + gboolean is_r4 = fsig->params [0]->type == MONO_TYPE_R4; + if (!strcmp (cmethod->name, "MaxNumber")) + opcode = is_r4 ? OP_RMAXNUM : OP_FMAXNUM; + else if (!strcmp (cmethod->name, "MinNumber")) + opcode = is_r4 ? OP_RMINNUM : OP_FMINNUM; + } + if (opcode) { + MONO_INST_NEW (cfg, ins, opcode); + ins->type = STACK_R8; + ins->dreg = mono_alloc_dreg (cfg, (MonoStackType)ins->type); + ins->sreg1 = args [0]->dreg; + ins->sreg2 = args [1]->dreg; + MONO_ADD_INS (cfg->cbb, ins); + } + } + if (in_corlib && !strcmp (m_class_get_name (cmethod->klass), "SpanHelpers")) { if (!strcmp (cmethod->name, "Memmove") && fsig->param_count == 3 && m_type_is_byref (fsig->params [0]) && m_type_is_byref (fsig->params [1]) && !cmethod->is_inflated) { MonoBasicBlock *end_bb; diff --git a/src/mono/mono/mini/llvm-intrinsics.h b/src/mono/mono/mini/llvm-intrinsics.h index a4f73dc8c823ad..fb6dd070b5cb13 100644 --- a/src/mono/mono/mini/llvm-intrinsics.h +++ b/src/mono/mono/mini/llvm-intrinsics.h @@ -88,6 +88,7 @@ INTRINS_OVR(POW, pow, Generic, LLVMDoubleType ()) INTRINS_OVR(EXP, exp, Generic, LLVMDoubleType ()) INTRINS_OVR(EXPF, exp, Generic, LLVMFloatType ()) INTRINS_OVR(LOG, log, Generic, LLVMDoubleType ()) +INTRINS_OVR(LOGF, log, Generic, LLVMFloatType ()) INTRINS_OVR(LOG2, log2, Generic, LLVMDoubleType ()) INTRINS_OVR(LOG2F, log2, Generic, LLVMFloatType ()) INTRINS_OVR(LOG10, log10, Generic, LLVMDoubleType ()) @@ -96,6 +97,18 @@ INTRINS_OVR(TRUNC, trunc, Generic, LLVMDoubleType ()) INTRINS_OVR(TRUNCF, trunc, Generic, LLVMFloatType ()) INTRINS_OVR(COPYSIGN, copysign, Generic, LLVMDoubleType ()) INTRINS_OVR(COPYSIGNF, copysign, Generic, LLVMFloatType ()) + /* + * IEEE 754-2008 minNum/maxNum (NaN-suppressing). When exactly one operand + * is NaN they return the other; when both are NaN they return NaN. This is + * what Math.MinNumber/MaxNumber (and the MathF.MinNumber/MaxNumber forwarders) + * are documented to do, and on AArch64 these lower to single fminnm/fmaxnm + * instructions. Use llvm.minimum/maximum (see above) for the NaN-propagating + * Math.Min/Math.Max instead. + */ +INTRINS_OVR(MINNUM, minnum, Generic, LLVMDoubleType ()) +INTRINS_OVR(MINNUMF, minnum, Generic, LLVMFloatType ()) +INTRINS_OVR(MAXNUM, maxnum, Generic, LLVMDoubleType ()) +INTRINS_OVR(MAXNUMF, maxnum, Generic, LLVMFloatType ()) INTRINS_OVR(EXPECT_I8, expect, Generic, LLVMInt8Type ()) INTRINS_OVR(EXPECT_I1, expect, Generic, LLVMInt1Type ()) INTRINS_OVR(CTPOP_I32, ctpop, Generic, LLVMInt32Type ()) diff --git a/src/mono/mono/mini/mini-llvm.c b/src/mono/mono/mini/mini-llvm.c index e68ce5ba8274b4..14c0a785846678 100644 --- a/src/mono/mono/mini/mini-llvm.c +++ b/src/mono/mono/mini/mini-llvm.c @@ -7494,6 +7494,13 @@ MONO_RESTORE_WARNING values [ins->dreg] = call_intrins (ctx, INTRINS_LOG, args, dname); break; } + case OP_LOGF: { + LLVMValueRef args [1]; + + args [0] = convert (ctx, lhs, LLVMFloatType ()); + values [ins->dreg] = call_intrins (ctx, INTRINS_LOGF, args, dname); + break; + } case OP_TRUNC: { LLVMValueRef args [1]; @@ -7703,6 +7710,31 @@ MONO_RESTORE_WARNING break; } + case OP_FMINNUM: + case OP_FMAXNUM: + case OP_RMINNUM: + case OP_RMAXNUM: { + /* + * IEEE 754-2008 minNum/maxNum (NaN-suppressing). Maps directly to + * llvm.minnum/maxnum, which is what Math.MinNumber/MaxNumber (and the + * MathF.MinNumber/MaxNumber forwarders) specify. On AArch64 this + * lowers to a single fminnm/fmaxnm instruction. + */ + gboolean is_r4 = ins->opcode == OP_RMINNUM || ins->opcode == OP_RMAXNUM; + LLVMTypeRef t = is_r4 ? LLVMFloatType () : LLVMDoubleType (); + LLVMValueRef args [2] = { convert (ctx, lhs, t), convert (ctx, rhs, t) }; + IntrinsicId iid; + switch (ins->opcode) { + case OP_FMAXNUM: iid = INTRINS_MAXNUM; break; + case OP_FMINNUM: iid = INTRINS_MINNUM; break; + case OP_RMAXNUM: iid = INTRINS_MAXNUMF; break; + case OP_RMINNUM: iid = INTRINS_MINNUMF; break; + default: g_assert_not_reached (); break; + } + values [ins->dreg] = call_intrins (ctx, iid, args, dname); + break; + } + /* * See the ARM64 comment in mono/utils/atomic.h for an explanation of why this * hack is necessary (for now). diff --git a/src/mono/mono/mini/mini-ops.h b/src/mono/mono/mini/mini-ops.h index 1971c7286ec86c..84238042808ead 100644 --- a/src/mono/mono/mini/mini-ops.h +++ b/src/mono/mono/mini/mini-ops.h @@ -684,9 +684,13 @@ MINI_OP(OP_LMIN, "long_min", LREG, LREG, LREG) MINI_OP(OP_LMAX, "long_max", LREG, LREG, LREG) MINI_OP(OP_RMAX, "rmax", FREG, FREG, FREG) MINI_OP(OP_RMIN, "rmin", FREG, FREG, FREG) +MINI_OP(OP_RMAXNUM, "rmaxnum", FREG, FREG, FREG) +MINI_OP(OP_RMINNUM, "rminnum", FREG, FREG, FREG) MINI_OP(OP_RPOW, "rpow", FREG, FREG, FREG) MINI_OP(OP_FMAX, "fmax", FREG, FREG, FREG) MINI_OP(OP_FMIN, "fmin", FREG, FREG, FREG) +MINI_OP(OP_FMAXNUM, "fmaxnum", FREG, FREG, FREG) +MINI_OP(OP_FMINNUM, "fminnum", FREG, FREG, FREG) MINI_OP(OP_FPOW, "fpow", FREG, FREG, FREG) MINI_OP(OP_RCOPYSIGN,"rcopysign", FREG, FREG, FREG) MINI_OP(OP_FCOPYSIGN,"fcopysign", FREG, FREG, FREG) @@ -740,6 +744,7 @@ MINI_OP(OP_LOG2, "log2", FREG, FREG, NONE) MINI_OP(OP_LOG2F, "log2f", FREG, FREG, NONE) MINI_OP(OP_LOG10, "log10", FREG, FREG, NONE) MINI_OP(OP_LOG10F, "log10f", FREG, FREG, NONE) +MINI_OP(OP_LOGF, "logf", FREG, FREG, NONE) MINI_OP(OP_TRUNC, "trunc", FREG, FREG, NONE) MINI_OP(OP_TRUNCF, "truncf", FREG, FREG, NONE) MINI_OP(OP_ABSF, "absf", FREG, FREG, NONE) From 24f3b7e894a1964980bc08ebbd062ae458bcdea0 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 22 Jun 2026 10:31:19 -0500 Subject: [PATCH 2/2] Address review: recognize Single/Double.Abs and fix comment wording Two changes in response to review feedback on #129699: * Add `Abs` recognition to the new Single/Double intrinsic block (Tanner's review comment). `Single.Abs(float)` and `Double.Abs(double)` are `[Intrinsic]` BCL forwarders to `MathF.Abs`/`Math.Abs`. Today on Mono these usually inline into the existing MathF/Math recognition, but adding direct recognition matches RyuJIT's `lookupPrimitiveFloatNamedIntrinsic` pattern (one lookup path regardless of which entry point a user reaches for) and keeps the lowering working even when the JIT inliner declines. Reuses the existing `OP_ABSF`/`OP_ABS` opcodes + `INTRINS_ABSF`/`INTRINS_FABS` lowering -- no new infrastructure. * Fix incorrect comment wording in three files (Copilot review nits). The MinNumber/MaxNumber comments referenced "Math.MinNumber/MaxNumber" and "MathF.MinNumber/MaxNumber forwarders", but those APIs don't exist on Math/MathF -- only on the primitive Single/Double/Half types via INumber. Also corrects a "numNum/maxNum" typo to "minNum/maxNum" in intrinsics.c. Build verification: `./build.sh mono+libs -os osx -arch arm64 -c Release` clean (0 errors, 0 warnings) on osx-arm64. No behavioral validation re-run -- the Abs recognition reuses already-validated `OP_ABSF`/`OP_ABS` lowering, and the comment changes are text-only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/mono/mono/mini/intrinsics.c | 26 ++++++++++++++++++++------ src/mono/mono/mini/llvm-intrinsics.h | 3 ++- src/mono/mono/mini/mini-llvm.c | 7 ++++--- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/src/mono/mono/mini/intrinsics.c b/src/mono/mono/mini/intrinsics.c index d30c5e67c4045d..a9b62e8d36e3dd 100644 --- a/src/mono/mono/mini/intrinsics.c +++ b/src/mono/mono/mini/intrinsics.c @@ -290,12 +290,25 @@ llvm_emit_inst_for_method (MonoCompile *cfg, MonoMethod *cmethod, MonoMethodSign } if (in_corlib && (!strcmp (m_class_get_name (cmethod->klass), "Single") || !strcmp (m_class_get_name (cmethod->klass), "Double"))) { - // Recognize the IEEE 754-2008 numNum/maxNum helpers on Single/Double. These - // live on the primitive types (forwarded from INumber), not on - // Math/MathF. Lower them to llvm.minnum/llvm.maxnum (NaN-suppressing), - // which matches the spec ("if either is NaN, return the non-NaN; if both - // are NaN, return NaN") and on AArch64 maps to a single fminnm/fmaxnm. + // Recognize a handful of helpers on the primitive `Single` / `Double` types + // (forwarded from INumberBase / INumber). These don't have + // corresponding APIs on Math/MathF -- e.g. there is no `Math.MinNumber` -- + // so the existing Math/MathF block above doesn't catch them. + // + // * MinNumber / MaxNumber: IEEE 754-2008 minNum / maxNum (NaN-suppressing). + // Lower to llvm.minnum / llvm.maxnum, which on AArch64 maps to a single + // fminnm / fmaxnm instruction; matches the BCL spec "if either is NaN + // return the non-NaN; if both are NaN return NaN". + // * Abs: BCL forwarder to MathF.Abs / Math.Abs. Today this usually inlines + // into the Math/MathF recognition above, but adding direct recognition + // keeps the lowering working even if the JIT inliner declines. opcode = 0; + if (fsig->param_count == 1 && + (fsig->params [0]->type == MONO_TYPE_R4 || fsig->params [0]->type == MONO_TYPE_R8)) { + gboolean is_r4 = fsig->params [0]->type == MONO_TYPE_R4; + if (!strcmp (cmethod->name, "Abs")) + opcode = is_r4 ? OP_ABSF : OP_ABS; + } if (fsig->param_count == 2 && fsig->params [0]->type == fsig->params [1]->type && (fsig->params [0]->type == MONO_TYPE_R4 || fsig->params [0]->type == MONO_TYPE_R8)) { @@ -310,7 +323,8 @@ llvm_emit_inst_for_method (MonoCompile *cfg, MonoMethod *cmethod, MonoMethodSign ins->type = STACK_R8; ins->dreg = mono_alloc_dreg (cfg, (MonoStackType)ins->type); ins->sreg1 = args [0]->dreg; - ins->sreg2 = args [1]->dreg; + if (fsig->param_count > 1) + ins->sreg2 = args [1]->dreg; MONO_ADD_INS (cfg->cbb, ins); } } diff --git a/src/mono/mono/mini/llvm-intrinsics.h b/src/mono/mono/mini/llvm-intrinsics.h index fb6dd070b5cb13..0b00d40b1e2485 100644 --- a/src/mono/mono/mini/llvm-intrinsics.h +++ b/src/mono/mono/mini/llvm-intrinsics.h @@ -100,7 +100,8 @@ INTRINS_OVR(COPYSIGNF, copysign, Generic, LLVMFloatType ()) /* * IEEE 754-2008 minNum/maxNum (NaN-suppressing). When exactly one operand * is NaN they return the other; when both are NaN they return NaN. This is - * what Math.MinNumber/MaxNumber (and the MathF.MinNumber/MaxNumber forwarders) + * what `float.MinNumber` / `double.MinNumber` (and the Max variants, + * surfaced via INumber on the primitive Single/Double/Half types) * are documented to do, and on AArch64 these lower to single fminnm/fmaxnm * instructions. Use llvm.minimum/maximum (see above) for the NaN-propagating * Math.Min/Math.Max instead. diff --git a/src/mono/mono/mini/mini-llvm.c b/src/mono/mono/mini/mini-llvm.c index 14c0a785846678..9e33b23de7c1c4 100644 --- a/src/mono/mono/mini/mini-llvm.c +++ b/src/mono/mono/mini/mini-llvm.c @@ -7716,9 +7716,10 @@ MONO_RESTORE_WARNING case OP_RMAXNUM: { /* * IEEE 754-2008 minNum/maxNum (NaN-suppressing). Maps directly to - * llvm.minnum/maxnum, which is what Math.MinNumber/MaxNumber (and the - * MathF.MinNumber/MaxNumber forwarders) specify. On AArch64 this - * lowers to a single fminnm/fmaxnm instruction. + * llvm.minnum/maxnum, which is what `float.MinNumber` / + * `double.MinNumber` (and the Max variants, surfaced via + * INumber on the primitive Single/Double/Half types) specify. + * On AArch64 this lowers to a single fminnm/fmaxnm instruction. */ gboolean is_r4 = ins->opcode == OP_RMINNUM || ins->opcode == OP_RMAXNUM; LLVMTypeRef t = is_r4 ? LLVMFloatType () : LLVMDoubleType ();