Skip to content

[LangRef] Clarify specification for float min/max operations#172012

Merged
nikic merged 18 commits intollvm:mainfrom
nikic:langref-minmax
Jan 23, 2026
Merged

[LangRef] Clarify specification for float min/max operations#172012
nikic merged 18 commits intollvm:mainfrom
nikic:langref-minmax

Conversation

@nikic
Copy link
Copy Markdown
Contributor

@nikic nikic commented Dec 12, 2025

This implements some clarifications for the specification of floating point min/max operations based on the discussion in https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

  • Explicitly specify minnum and maxnum with an sNaN operand as non-deterministically either returning NaN or treating sNaN as qNaN. This was implied by our general NaN semantics, but is important to call out here due to the special behavior of sNaN.
  • Explicitly specify the same non-determinism for the minnum/maxnum based vector reductions as well.
  • Explicitly specify the meaning of nsz on float min/max ops. In particular, clarify that unlike normal nsz semantics, it does not allow introducing a zero with a different sign out of thin air.
  • Simplify the semantics comparison section. This now focuses only on NaN and signed zero behavior, but omits information about exceptions that is not relevant for these non-constrained intrinsics.

@llvmbot
Copy link
Copy Markdown
Member

llvmbot commented Dec 12, 2025

@llvm/pr-subscribers-llvm-ir

Author: Nikita Popov (nikic)

Changes

This implements some clarifications for the specification of floating point min/max operations based on the discussion in https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

  • Explicitly specify minnum and maxnum with an sNaN operand as non-deterministically either returning NaN or treating sNaN as qNaN. This was implied by our general NaN semantics, but is important to call out here due to the special behavior of sNaN.
  • Explicitly specify the same non-determinism for the minnum/maxnum based vector reductions as well.
  • Explicitly specify the meaning of nsz on float min/max ops. In particular, clarify that unlike normal nsz semantics, it does not allow introducing a zero with a different sign out of thin air.
  • Remove the semantics comparison section. I tried to adjust this, but there's just so many caveats in here that it's hard to create something that is both concise and correct.

Full diff: https://github.com/llvm/llvm-project/pull/172012.diff

1 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+78-140)
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 5fa3a4ebb2472..3ce5244a39b31 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -17288,96 +17288,6 @@ The returned value is completely identical to the input except for the sign bit;
 in particular, if the input is a NaN, then the quiet/signaling bit and payload
 are perfectly preserved.
 
-.. _i_fminmax_family:
-
-'``llvm.min.*``' Intrinsics Comparation
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-Standard:
-"""""""""
-
-IEEE754 and ISO C define some min/max operations, and they have some differences
-on working with qNaN/sNaN and +0.0/-0.0. Here is the list:
-
-.. list-table::
-   :header-rows: 2
-
-   * - ``ISO C``
-     - fmin/fmax
-     - fmininum/fmaximum
-     - fminimum_num/fmaximum_num
-
-   * - ``IEEE754``
-     - minNum/maxNum (2008)
-     - minimum/maximum (2019)
-     - minimumNumber/maximumNumber (2019)
-
-   * - ``+0.0 vs -0.0``
-     - either one
-     - +0.0 > -0.0
-     - +0.0 > -0.0
-
-   * - ``NUM vs sNaN``
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-     - NUM, invalid exception
-
-   * - ``qNaN vs sNaN``
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-
-   * - ``NUM vs qNaN``
-     - NUM, no exception
-     - qNaN, no exception
-     - NUM, no exception
-
-LLVM Implementation:
-""""""""""""""""""""
-
-LLVM implements all ISO C flavors as listed in this table, except in the
-default floating-point environment exceptions are ignored. The constrained
-versions of the intrinsics respect the exception behavior.
-
-.. list-table::
-   :header-rows: 1
-   :widths: 16 28 28 28
-
-   * - Operation
-     - minnum/maxnum
-     - minimum/maximum
-     - minimumnum/maximumnum
-
-   * - ``NUM vs qNaN``
-     - NUM, no exception
-     - qNaN, no exception
-     - NUM, no exception
-
-   * - ``NUM vs sNaN``
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-     - NUM, invalid exception
-
-   * - ``qNaN vs sNaN``
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-
-   * - ``sNaN vs sNaN``
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-     - qNaN, invalid exception
-
-   * - ``+0.0 vs -0.0``
-     - +0.0(max)/-0.0(min)
-     - +0.0(max)/-0.0(min)
-     - +0.0(max)/-0.0(min)
-
-   * - ``NUM vs NUM``
-     - larger(max)/smaller(min)
-     - larger(max)/smaller(min)
-     - larger(max)/smaller(min)
-
 .. _i_minnum:
 
 '``llvm.minnum.*``' Intrinsic
@@ -17413,30 +17323,26 @@ type.
 
 Semantics:
 """"""""""
-Follows the semantics of minNum in IEEE-754-2008, except that -0.0 < +0.0 for the purposes
-of this intrinsic. As for signaling NaNs, per the minNum semantics, if either operand is sNaN,
-the result is qNaN. This matches the recommended behavior for the libm
-function ``fmin``, although not all implementations have implemented these recommended behaviors.
 
-If either operand is a qNaN, returns the other non-NaN operand. Returns NaN only if both operands are
-NaN or if either operand is sNaN. Note that arithmetic on an sNaN doesn't consistently produce a qNaN,
-so arithmetic feeding into a minnum can produce inconsistent results. For example,
-``minnum(fadd(sNaN, -0.0), 1.0)`` can produce qNaN or 1.0 depending on whether ``fadd`` is folded.
+If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is
+qNaN and another operand is a number, returns the number. If both operands are
+numbers, returns the lesser of the two arguments. -0.0 is considered to be less
+than +0.0 for this intrinsic.
 
-IEEE-754-2008 defines minNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019
-defines :ref:`minimumNumber <i_minimumnum>`.
+If an operand is a signaling NaN, then the intrinsic will non-deterministically
+either:
 
-If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C
-and IEEE-754-2008: the result of ``minnum(-0.0, +0.0)`` may be either -0.0 or +0.0.
+ * Return a :ref:`NaN <floatnan>`.
+ * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will
+   behave the same as ``llvm.minimumnum``.
 
-Some architectures, such as ARMv8 (FMINNM), LoongArch (fmin), MIPSr6 (min.fmt), PowerPC/VSX (xsmindp),
-have instructions that match these semantics exactly; thus it is quite simple for these architectures.
-Some architectures have similar ones while they are not exact equivalent. Such as x86 implements ``MINPS``,
-which implements the semantics of C code ``a<b?a:b``: NUM vs qNaN always return qNaN. ``MINPS`` can be used
-if ``nsz`` and ``nnan`` are given.
+If the ``nsz`` flag is specified, ``llvm.minnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
 
-For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors,
-even in the same release of a single libm implementation.
+When used with the ``nsz`` flag, this intrinsics follows the semantics of
+``fmin`` in C.
 
 .. _i_maxnum:
 
@@ -17473,30 +17379,26 @@ type.
 
 Semantics:
 """"""""""
-Follows the semantics of maxNum in IEEE-754-2008, except that -0.0 < +0.0 for the purposes
-of this intrinsic. As for signaling NaNs, per the maxNum semantics, if either operand is sNaN,
-the result is qNaN. This matches the recommended behavior for the libm
-function ``fmax``, although not all implementations have implemented these recommended behaviors.
 
-If either operand is a qNaN, returns the other non-NaN operand. Returns NaN only if both operands are
-NaN or if either operand is sNaN. Note that arithmetic on an sNaN doesn't consistently produce a qNaN,
-so arithmetic feeding into a maxnum can produce inconsistent results. For example,
-``maxnum(fadd(sNaN, -0.0), 1.0)`` can produce qNaN or 1.0 depending on whether ``fadd`` is folded.
+If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is
+qNaN and another operand is a number, returns the number. If both operands are
+numbers, returns the greater of the two arguments. -0.0 is considered to be
+less than +0.0 for this intrinsic.
 
-IEEE-754-2008 defines maxNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019
-defines :ref:`maximumNumber <i_maximumnum>`.
+If an operand is a signaling NaN, then the intrinsic will non-deterministically
+either:
 
-If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C
-and IEEE-754-2008: the result of maxnum(-0.0, +0.0) may be either -0.0 or +0.0.
+ * Return a :ref:`NaN <floatnan>`.
+ * Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will
+   behave the same as ``llvm.maximumnum``.
 
-Some architectures, such as ARMv8 (FMAXNM), LoongArch (fmax), MIPSr6 (max.fmt), PowerPC/VSX (xsmaxdp),
-have instructions that match these semantics exactly; thus it is quite simple for these architectures.
-Some architectures have similar ones while they are not exact equivalent. Such as x86 implements ``MAXPS``,
-which implements the semantics of C code ``a>b?a:b``: NUM vs qNaN always return qNaN. ``MAXPS`` can be used
-if ``nsz`` and ``nnan`` are given.
+If the ``nsz`` flag is specified, ``llvm.maxnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
 
-For existing libc implementations, the behaviors of fmin may be quite different on sNaN and signed zero behaviors,
-even in the same release of a single libm implementation.
+When used with the ``nsz`` flag, this intrinsics follows the semantics of
+``fmax`` in C.
 
 .. _i_minimum:
 
@@ -17538,6 +17440,11 @@ of the two arguments. -0.0 is considered to be less than +0.0 for this
 intrinsic. Note that these are the semantics specified in the draft of
 IEEE 754-2019.
 
+If the ``nsz`` flag is specified, ``llvm.maximum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
 .. _i_maximum:
 
 '``llvm.maximum.*``' Intrinsic
@@ -17578,6 +17485,11 @@ of the two arguments. -0.0 is considered to be less than +0.0 for this
 intrinsic. Note that these are the semantics specified in the draft of
 IEEE 754-2019.
 
+If the ``nsz`` flag is specified, ``llvm.maximum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
 .. _i_minimumnum:
 
 '``llvm.minimumnum.*``' Intrinsic
@@ -17619,12 +17531,17 @@ one operand is NaN (including sNaN) and another operand is a number,
 return the number.  Otherwise returns the lesser of the two
 arguments. -0.0 is considered to be less than +0.0 for this intrinsic.
 
+If the ``nsz`` flag is specified, ``llvm.minimumnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
 Note that these are the semantics of minimumNumber specified in
 IEEE-754-2019 with the usual :ref:`signaling NaN <floatnan>` exception.
 
-It has some differences with '``llvm.minnum.*``':
-1)'``llvm.minnum.*``' will return qNaN if either operand is sNaN.
-2)'``llvm.minnum*``' may return either one if we compare +0.0 vs -0.0.
+This intrinsic differs from ``llvm.minnum`` in that it is guaranteed to treat
+sNaN the same way as qNaN. ``llvm.minnum`` will instead non-deterministically
+either act like ``llvm.minimumnum`` or return a :ref:`NaN <floatnan>`.
 
 .. _i_maximumnum:
 
@@ -17668,12 +17585,17 @@ another operand is a number, return the number.  Otherwise returns the
 greater of the two arguments. -0.0 is considered to be less than +0.0
 for this intrinsic.
 
+If the ``nsz`` flag is specified, ``llvm.maximumnum`` with one +0.0 and one
+-0.0 operand may non-deterministically return either operand. Contrary to normal
+``nsz`` semantics, if both operands have the same sign, the result must also
+have the same sign.
+
 Note that these are the semantics of maximumNumber specified in
 IEEE-754-2019  with the usual :ref:`signaling NaN <floatnan>` exception.
 
-It has some differences with '``llvm.maxnum.*``':
-1)'``llvm.maxnum.*``' will return qNaN if either operand is sNaN.
-2)'``llvm.maxnum*``' may return either one if we compare +0.0 vs -0.0.
+This intrinsic differs from ``llvm.maxnum`` in that it is guaranteed to treat
+sNaN the same way as qNaN. ``llvm.maxnum`` will instead non-deterministically
+either act like ``llvm.maximumnum`` or return a :ref:`NaN <floatnan>`.
 
 .. _int_copysign:
 
@@ -20379,9 +20301,17 @@ The '``llvm.vector.reduce.fmax.*``' intrinsics do a floating-point
 ``MAX`` reduction of a vector, returning the result as a scalar. The return type
 matches the element-type of the vector input.
 
-This instruction has the same comparison semantics as the '``llvm.maxnum.*``'
-intrinsic.  If the intrinsic call has the ``nnan`` fast-math flag, then the
-operation can assume that NaNs are not present in the input vector.
+This instruction has the same comparison and ``nsz`` semantics as the
+'``llvm.maxnum.*``' intrinsic.
+
+If any of the vector elements is a signaling NaN, the intrinsic will
+non-deterministically either:
+
+ * Return a :ref:`NaN <floatnan>`.
+ * Treat the signaling NaN as a quiet NaN.
+
+If the intrinsic call has the ``nnan`` fast-math flag, then the operation can
+assume that NaNs are not present in the input vector.
 
 Arguments:
 """"""""""
@@ -20408,9 +20338,17 @@ The '``llvm.vector.reduce.fmin.*``' intrinsics do a floating-point
 ``MIN`` reduction of a vector, returning the result as a scalar. The return type
 matches the element-type of the vector input.
 
-This instruction has the same comparison semantics as the '``llvm.minnum.*``'
-intrinsic. If the intrinsic call has the ``nnan`` fast-math flag, then the
-operation can assume that NaNs are not present in the input vector.
+This instruction has the same comparison and ``nsz`` semantics as the
+'``llvm.minnum.*``' intrinsic.
+
+If any of the vector elements is a signaling NaN, the intrinsic will
+non-deterministically either:
+
+ * Return a :ref:`NaN <floatnan>`.
+ * Treat the signaling NaN as a quiet NaN.
+
+If the intrinsic call has the ``nnan`` fast-math flag, then the operation can
+assume that NaNs are not present in the input vector.
 
 Arguments:
 """"""""""

@RalfJung
Copy link
Copy Markdown
Contributor

This sounds good to me! I don't have a strong opinion on whether minnum should default to nsz behavior (to match IEEE-754) or not.

Copy link
Copy Markdown
Member

@jyknight jyknight left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo wording tweaks.

Copy link
Copy Markdown
Contributor

@jcranmer-intel jcranmer-intel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do somewhat miss having the table that organizes all of the different flavors of min/max operations to help identify which ones are in use.

That said, I've long been considering the need to have a dedicated docs page on floating-point semantics for LLVM IR and what different stakeholders need to understand about those semantics (like the existing atomics page), and the table would definitely fit there better. This entire saga is upping work on that document in my priority list, but nothing's going to happen before the end of the calendar year.

@arsenm arsenm added the floating-point Floating-point math label Dec 12, 2025
Copy link
Copy Markdown
Contributor

@arsenm arsenm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if it's nondeterministic, what do we constant fold to for a signaling nan?

@RalfJung
Copy link
Copy Markdown
Contributor

RalfJung commented Dec 12, 2025

We can do either. Is there one option that's better for backends / follow-up optimizations? What other operations do (pow) is to fold all NaN the same way -- that also has the advantage that one can fold without knowing which kind of NaN it is.

We also need to ensure that scalar evolution is aware of all this non-determinism and doesn't try to predict what these operations do. With nsz they are non-deterministic even for non-NaN inputs, which is unusual.

@arsenm
Copy link
Copy Markdown
Contributor

arsenm commented Dec 12, 2025

We can do either. Is there one option that's better for backends / follow-up optimizations?

Folding to the qNaN will allow more downstream operations to fold, since just about everything else simply propagates input nans

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 12, 2025

So if it's nondeterministic, what do we constant fold to for a signaling nan?

Either option would be legal. I think using the IEEE 2008 sNaN semantics might be slightly preferable to match the constrained variants. Or we could also uphold #170181 as a permanent rather than temporary change, and just not fold sNaN arguments for these intrinsics at all.

We also need to ensure that scalar evolution is aware of all this non-determinism and doesn't try to predict what these operations do. With nsz they are non-deterministic even for non-NaN inputs, which is unusual.

Currently we conservatively assume that all FP libcalls/intrinsics are non-deterministic:

// Conservatively assume that floating-point libcalls may be
// non-deterministic.
Type *Ty = F->getReturnType();
if (!AllowNonDeterministic && Ty->isFPOrFPVectorTy())
return nullptr;

@RalfJung
Copy link
Copy Markdown
Contributor

RalfJung commented Dec 12, 2025

Folding to the qNaN will allow more downstream operations to fold, since just about everything else simply propagates input nans

It could also be: fold to QNaN if the input is known to be an SNaN, otherwise fold to other operand if the input is just known to be any NaN. (Not sure if that's something that can happen with how LLVM represents partial knowledge of floating-point values.)

Comment on lines +17327 to +17337
If both operands are qNaNs, returns a :ref:`NaN <floatnan>`. If one operand is
qNaN and another operand is a number, returns the number. If both operands are
numbers, returns the lesser of the two arguments. -0.0 is considered to be less
than +0.0 for this intrinsic.

IEEE-754-2008 defines minNum, and it was removed in IEEE-754-2019. As the replacement, IEEE-754-2019
defines :ref:`minimumNumber <i_minimumnum>`.
If an operand is a signaling NaN, then the intrinsic will non-deterministically
either:

If the intrinsic is marked with the nsz attribute, then the effect is as in the definition in C
and IEEE-754-2008: the result of ``minnum(-0.0, +0.0)`` may be either -0.0 or +0.0.
* Return a :ref:`NaN <floatnan>`.
* Or treat the signaling NaN as a quiet NaN. In this case the intrinsic will
behave the same as ``llvm.minimumnum``.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It took me a while to work out what's allowed if both inputs are sNaN. Does it have to return a qNaN? On x86, this operation can be implemented in a much faster way if we allow a signaling NaN input to be passed through to the output. I think this phrasing allows that behavior, but you have to read between the lines a bit.

Right now, it's clear that if both operands are qNaN, it returns "a NaN" (following LLVM semantics). If one operand is sNaN and the other is a number, we are free to return either the number (following llvm.minimumnum) or "a NaN" (like IEEE754-2008, but we don't have to quiet the NaN).

However, it doesn't explicitly enumerate the case where both operands are sNaN. If we assume it's a subset of the case where "an operand is a signaling NaN", we can non-deterministically choose to return "a NaN" and pass through the sNaN. I think it would be helpful to make that case explicit.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that returning "a NaN" also allows passing through the SNaN, so I think both possible interpretations of the two-SNaN case yield the same overall semantics.

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 16, 2025

I've tried to address the review feedback, please take a look.

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 16, 2025

Based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/61?u=nikic, I've dropped the requirement for zero ordering from maxnum/minnum.

So basically now minnum == fmin == IEEE 754-2008 minNum modulo usual sNaN exception.

I can revert that commit if the discussion reaches a different conclusion...

have the same sign.

Note that these are the semantics of minimumNumber specified in
IEEE-754-2019 with the usual :ref:`signaling NaN <floatnan>` exception.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, there's now a mix of "IEEE-754" and "IEEE 754". Not sure that it matters...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, there is a mix of both forms throughout the document. We should normalize to one of them in a separate change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see at present 3 uses of IEEE754, 6 of IEEE 754, 32 of IEEE-754, 0 of any flavor of ISO/IEC 60559.

(Of the three forms, while IEEE-754 is the dominant form right now, IEEE 754- appears to be the most correct form.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#174721 normalizes to IEEE 754.

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 17, 2025

I can revert that commit if the discussion reaches a different conclusion...

Okay, I did end up reverting this based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/69?u=nikic.

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 17, 2025

I've added a warning to indicate that the specified signed zero ordering hasn't been implemented yet.

@phoebewang
Copy link
Copy Markdown
Contributor

I can revert that commit if the discussion reaches a different conclusion...

Okay, I did end up reverting this based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/69?u=nikic.

@nikic Can we revert it to the last commit? It looks to me the only motivation is to use signed zero minnum/maxnum for some optimizations. However, there are several problems/concerns need to be addressed regarding it:

  • The choice between maxnum and maximumnum. Considering some language like Rust have deterministic sNaN requirement, a general optimizaiton should not break the sNaN determinism;
  • The correctness. Considering the status of signed zero is widely not respected among backend and library implementations, we should raise red flags for such optimizaitons through the definition instead of a warning;
  • The performance. @arsenm explain the reason of choosing maxnum is cost of quieting sNaN, which concerns me because signed zero also has cost on x86, which seems not take into account equally;

@valadaptive
Copy link
Copy Markdown
Contributor

I can revert that commit if the discussion reaches a different conclusion...

Okay, I did end up reverting this based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/69?u=nikic.

@nikic Can we revert it to the last commit? It looks to me the only motivation is to use signed zero minnum/maxnum for some optimizations. However, there are several problems/concerns need to be addressed regarding it:

* The choice between maxnum and maximumnum. Considering some language like Rust have deterministic sNaN requirement, a general optimizaiton should not break the sNaN determinism;

* The correctness. Considering the status of signed zero is widely not respected among backend and library implementations, we should raise red flags for such optimizaitons through the definition instead of a warning;

* The performance. @arsenm explain the reason of choosing maxnum is cost of quieting sNaN, which concerns me because signed zero also has cost on x86, which seems not take into account equally;

Everyone else seems to have reached a consensus. The previous behavior is still available using the nsz flag, and Clang and Rust can and should add that flag. Using minnum/maxnum without nsz is (AFAIK) implementable across architectures, so we don't need to worry about the semantics of fmin/fmax libcalls.

@phoebewang
Copy link
Copy Markdown
Contributor

I can revert that commit if the discussion reaches a different conclusion...

Okay, I did end up reverting this based on https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006/69?u=nikic.

@nikic Can we revert it to the last commit? It looks to me the only motivation is to use signed zero minnum/maxnum for some optimizations. However, there are several problems/concerns need to be addressed regarding it:

* The choice between maxnum and maximumnum. Considering some language like Rust have deterministic sNaN requirement, a general optimizaiton should not break the sNaN determinism;

* The correctness. Considering the status of signed zero is widely not respected among backend and library implementations, we should raise red flags for such optimizaitons through the definition instead of a warning;

* The performance. @arsenm explain the reason of choosing maxnum is cost of quieting sNaN, which concerns me because signed zero also has cost on x86, which seems not take into account equally;

Everyone else seems to have reached a consensus. The previous behavior is still available using the nsz flag, and Clang and Rust can and should add that flag. Using minnum/maxnum without nsz is (AFAIK) implementable across architectures, so we don't need to worry about the semantics of fmin/fmax libcalls.

I'm talking about this point:

Matt has some use case for this in mind in math library optimization (if I understood right, related to the sign bit of the result of maxnum(x, 0.0), which may be set if x == -0.0 without the ordered zero semantics).

@valadaptive
Copy link
Copy Markdown
Contributor

I'm talking about this point:

Matt has some use case for this in mind in math library optimization (if I understood right, related to the sign bit of the result of maxnum(x, 0.0), which may be set if x == -0.0 without the ordered zero semantics).

I certainly agree that we shouldn't do such optimizations until the signed-zero behavior is actually implemented, and that they should also not be applied if the nsz flag is passed.

I don't see why that means we shouldn't expose signed-zero ordering at all.

Matt's proposed optimization is actually illustrative here. I don't know in particular what we can optimize by statically knowing the sign bit, but it applies when one operand is statically known to be +0.0.

In that case, having the nsz flag is actually a good thing. Recall that the x86 implementation uses maxps, which behaves like y < x ? x : y. Other fallback implementations, for architectures without a floating-point min/max operation, will do the same thing but with an explicit compare+select.

In such a scenario, we can actually refine maxnum nsz (x, 0.0) to maxnum(x, 0.0) without any performance loss at all. The operation lowers to 0.0 < x ? x : 0.0, and it's easy to see that the result is always positive, so we don't need to fix up the sign bit afterwards. The x86 backend already performs this optimization.

Consider instead a scenario where maxnum had no signed-zero ordering semantics. If we wanted to statically know the sign bit of maxnum(x, 0.0), we'd have to refine it to maximumnum(x, 0.0). x could be a signaling NaN, so on platforms that implement the IEEE754-2008 operations, like AArch64, we would have to "canonicalize" x beforehand. This is an unnecessary performance cost, since the original maxnum doesn't require us to handle signaling NaN.

Basically, if we have a version of maxnum that orders signed zeroes properly, we can do more optimizations. Even if the frontends choose not to emit it and always add the nsz flag, we can safely drop nsz if we know it helps with performance.

@phoebewang
Copy link
Copy Markdown
Contributor

I certainly agree that we shouldn't do such optimizations until the signed-zero behavior is actually implemented, and that they should also not be applied if the nsz flag is passed.

I don't see why that means we shouldn't expose signed-zero ordering at all.

At least, it should be done in order:

  • Correct LangRef to respect the current status of non-determinism signed-zero semantic among implementation;
  • Show the details and rationality of optimizations leveraging signed-zero minnum/maxnum, make sure no regressions on targets don’t have efficient signed zero handling;
  • Make all the lowering paths including -Oz, soft-float targets etc. signed-zero determinism;
  • Then change the semantic definition and implement optimizaitons;

In such a scenario, we can actually refine maxnum nsz (x, 0.0) to maxnum(x, 0.0) without any performance loss at all. The operation lowers to 0.0 < x ? x : 0.0, and it's easy to see that the result is always positive, so we don't need to fix up the sign bit afterwards. The x86 backend already performs this optimization.

It's just one of the lowering pass, and specific to x86. At least, libcall doesn't guarantee it: https://godbolt.org/z/asvK5n5aM

Consider instead a scenario where maxnum had no signed-zero ordering semantics. If we wanted to statically know the sign bit of maxnum(x, 0.0), we'd have to refine it to maximumnum(x, 0.0). x could be a signaling NaN, so on platforms that implement the IEEE754-2008 operations, like AArch64, we would have to "canonicalize" x beforehand. This is an unnecessary performance cost, since the original maxnum doesn't require us to handle signaling NaN.

Signaling NaN should matter to all, or none. That says, we should not optimizate any thing to maxnum if we really respect signaling NaN.

Basically, if we have a version of maxnum that orders signed zeroes properly, we can do more optimizations. Even if the frontends choose not to emit it and always add the nsz flag, we can safely drop nsz if we know it helps with performance.

I'm cautious about it. From x86 baackend PoV, they are basically regression if not neutral. In fact, I agree with the opposite point from @ActuallyaDeviloper :

I think we could aggressively infer the no-signed-zeros flag for many instructions. In particular, any instruction whos result feeds only into an addition/subtraction with provably non-zero argument or maximum/minimum with argument >/< 0. This can be recursively inferred backwards and across almost any operation (besides divisions).

@nikic
Copy link
Copy Markdown
Contributor Author

nikic commented Dec 22, 2025

At least, it should be done in order:

This is why this patch documents that the intrinsics have ordered zero behavior, but includes a warning that this behavior has not been implemented yet.

This will allow people to actually start working on making that a reality without getting bogged down in the 10th discussion on this topic, while at the same time making it clear that you can't rely on this yet.

Of course, if we want to actually make optimizations that involve dropping the nsz flag, we need to be careful about what the performance effect would be on targets that don't support that natively. I did find @valadaptive's point that maxnum(x, 0.0) in particular admits an efficient X86 lowering even without nsz quite interesting -- that would make the performance argument for that case simpler.

(I really don't want this thread to get bogged down in discussions of a specific potential optimization -- it was only mentioned as an example that maxnum without nsz can have practical application even if frontends don't emit it directly.)

@phoebewang
Copy link
Copy Markdown
Contributor

This will allow people to actually start working on making that a reality without getting bogged down in the 10th discussion on this topic, while at the same time making it clear that you can't rely on this yet.

I think it's more important to answer the right or wrong question before actually doing any follow up works. Discussions mean it's controversial enough. And I think it's hasty to draw a conclusion the vague optimizaiton worths the works ahead.

Note, I'm not opposed to use maximumnum for a positive optimizaiton. The questions are: if it's positive? why don't moving it to target specific code if the optimizaiton highly rely on target features (e.g., IEEE754-2008, signed zero etc.)? is it really correct to optimize none maxnum instruction to maxnum if sNaN matters? and if not, why don't we modify maximumnum the same sNaN non-determinism as maxnum as an alternative?

@RalfJung
Copy link
Copy Markdown
Contributor

There's no "right" or "wrong", just trade-offs. And the trade-offs will be much more clear once actual patches get written, which currently is blocked on having a coherent documented semantics to review the patches against.

Is there some way to declare minnum without nsz as "unstable" in some sense so that frontends should not use it and it can be removed again in the future if it turns out to not be useful?

Copy link
Copy Markdown
Contributor

@fhahn fhahn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this PR at the LLVM area team meeting today. We found that there is majority consensus that this is a step forward in the right direction and this PR should go ahead.

LGTM

@nikic nikic merged commit 6bae2a9 into llvm:main Jan 23, 2026
11 checks passed
@nikic nikic deleted the langref-minmax branch January 23, 2026 13:35
Harrish92 pushed a commit to Harrish92/llvm-project that referenced this pull request Jan 23, 2026
…2012)

This implements some clarifications for the specification of floating
point min/max operations based on the discussion in
https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

* Explicitly specify minnum and maxnum with an sNaN operand as
non-deterministically either returning NaN or treating sNaN as qNaN.
This was implied by our general NaN semantics, but is important to call
out here due to the special behavior of sNaN.
* Explicitly specify the same non-determinism for the minnum/maxnum
based vector reductions as well.
* Explicitly specify the meaning of nsz on float min/max ops. In
particular, clarify that unlike normal nsz semantics, it does not allow
introducing a zero with a different sign out of thin air.
* Simplify the semantics comparison section. This now focuses only on
NaN and signed zero behavior, but omits information about exceptions
that is not relevant for these non-constrained intrinsics.
Harrish92 pushed a commit to Harrish92/llvm-project that referenced this pull request Jan 24, 2026
…2012)

This implements some clarifications for the specification of floating
point min/max operations based on the discussion in
https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

* Explicitly specify minnum and maxnum with an sNaN operand as
non-deterministically either returning NaN or treating sNaN as qNaN.
This was implied by our general NaN semantics, but is important to call
out here due to the special behavior of sNaN.
* Explicitly specify the same non-determinism for the minnum/maxnum
based vector reductions as well.
* Explicitly specify the meaning of nsz on float min/max ops. In
particular, clarify that unlike normal nsz semantics, it does not allow
introducing a zero with a different sign out of thin air.
* Simplify the semantics comparison section. This now focuses only on
NaN and signed zero behavior, but omits information about exceptions
that is not relevant for these non-constrained intrinsics.
valadaptive added a commit that referenced this pull request Jan 25, 2026
…ven without `nsz` (#177828)

This restriction was originally added in
https://reviews.llvm.org/D143256, with the given justification:

> Currently, in TargetLowering, if the target does not support fminnum,
we lower to fminimum if neither operand could be a NaN. But this isn't
quite correct because fminnum and fminimum treat +/-0 differently; so,
we need to prove that one of the operands isn't a zero.

As far as I can tell, this was never correct. Before
#172012, `minnum` and `maxnum`
were nondeterministic with regards to signed zero, so it's always been
perfectly legal to lower them to operations that order signed zeroes.
llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Jan 25, 2026
…/FMAXIMUM even without `nsz` (#177828)

This restriction was originally added in
https://reviews.llvm.org/D143256, with the given justification:

> Currently, in TargetLowering, if the target does not support fminnum,
we lower to fminimum if neither operand could be a NaN. But this isn't
quite correct because fminnum and fminimum treat +/-0 differently; so,
we need to prove that one of the operands isn't a zero.

As far as I can tell, this was never correct. Before
llvm/llvm-project#172012, `minnum` and `maxnum`
were nondeterministic with regards to signed zero, so it's always been
perfectly legal to lower them to operations that order signed zeroes.
HugoSilvaSantos pushed a commit to HugoSilvaSantos/arm-toolchain that referenced this pull request Jan 27, 2026
…ven without `nsz` (#177828)

This restriction was originally added in
https://reviews.llvm.org/D143256, with the given justification:

> Currently, in TargetLowering, if the target does not support fminnum,
we lower to fminimum if neither operand could be a NaN. But this isn't
quite correct because fminnum and fminimum treat +/-0 differently; so,
we need to prove that one of the operands isn't a zero.

As far as I can tell, this was never correct. Before
llvm/llvm-project#172012, `minnum` and `maxnum`
were nondeterministic with regards to signed zero, so it's always been
perfectly legal to lower them to operations that order signed zeroes.
Icohedron pushed a commit to Icohedron/llvm-project that referenced this pull request Jan 29, 2026
…2012)

This implements some clarifications for the specification of floating
point min/max operations based on the discussion in
https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

* Explicitly specify minnum and maxnum with an sNaN operand as
non-deterministically either returning NaN or treating sNaN as qNaN.
This was implied by our general NaN semantics, but is important to call
out here due to the special behavior of sNaN.
* Explicitly specify the same non-determinism for the minnum/maxnum
based vector reductions as well.
* Explicitly specify the meaning of nsz on float min/max ops. In
particular, clarify that unlike normal nsz semantics, it does not allow
introducing a zero with a different sign out of thin air.
* Simplify the semantics comparison section. This now focuses only on
NaN and signed zero behavior, but omits information about exceptions
that is not relevant for these non-constrained intrinsics.
Icohedron pushed a commit to Icohedron/llvm-project that referenced this pull request Jan 29, 2026
…ven without `nsz` (llvm#177828)

This restriction was originally added in
https://reviews.llvm.org/D143256, with the given justification:

> Currently, in TargetLowering, if the target does not support fminnum,
we lower to fminimum if neither operand could be a NaN. But this isn't
quite correct because fminnum and fminimum treat +/-0 differently; so,
we need to prove that one of the operands isn't a zero.

As far as I can tell, this was never correct. Before
llvm#172012, `minnum` and `maxnum`
were nondeterministic with regards to signed zero, so it's always been
perfectly legal to lower them to operations that order signed zeroes.
sshrestha-aa pushed a commit to sshrestha-aa/llvm-project that referenced this pull request Feb 4, 2026
…2012)

This implements some clarifications for the specification of floating
point min/max operations based on the discussion in
https://discourse.llvm.org/t/rfc-a-consistent-set-of-semantics-for-the-floating-point-minimum-and-maximum-operations/89006.

The key changes are:

* Explicitly specify minnum and maxnum with an sNaN operand as
non-deterministically either returning NaN or treating sNaN as qNaN.
This was implied by our general NaN semantics, but is important to call
out here due to the special behavior of sNaN.
* Explicitly specify the same non-determinism for the minnum/maxnum
based vector reductions as well.
* Explicitly specify the meaning of nsz on float min/max ops. In
particular, clarify that unlike normal nsz semantics, it does not allow
introducing a zero with a different sign out of thin air.
* Simplify the semantics comparison section. This now focuses only on
NaN and signed zero behavior, but omits information about exceptions
that is not relevant for these non-constrained intrinsics.
sshrestha-aa pushed a commit to sshrestha-aa/llvm-project that referenced this pull request Feb 4, 2026
…ven without `nsz` (llvm#177828)

This restriction was originally added in
https://reviews.llvm.org/D143256, with the given justification:

> Currently, in TargetLowering, if the target does not support fminnum,
we lower to fminimum if neither operand could be a NaN. But this isn't
quite correct because fminnum and fminimum treat +/-0 differently; so,
we need to prove that one of the operands isn't a zero.

As far as I can tell, this was never correct. Before
llvm#172012, `minnum` and `maxnum`
were nondeterministic with regards to signed zero, so it's always been
perfectly legal to lower them to operations that order signed zeroes.
LewisCrawford added a commit that referenced this pull request Mar 3, 2026
This reverts commit ea3fdc5.

Re-enable const-folding for maxnum/minnum in the middle-end, GlobalISel,
and SelectionDAG.

Re-enable optimizations that depend on maxnum/minnum sNaN semantics in
InstCombine and DAGCombiner.

Now that maxnum(x, sNaN) is specified to non-deterministically produce
either NaN or x, these constant-foldings and optimizations are now valid
again according to the newly clarified semantics in #172012 .
nasherm pushed a commit to nasherm/llvm-project that referenced this pull request Mar 3, 2026
…lvm#184125)

This reverts commit ea3fdc5.

Re-enable const-folding for maxnum/minnum in the middle-end, GlobalISel,
and SelectionDAG.

Re-enable optimizations that depend on maxnum/minnum sNaN semantics in
InstCombine and DAGCombiner.

Now that maxnum(x, sNaN) is specified to non-deterministically produce
either NaN or x, these constant-foldings and optimizations are now valid
again according to the newly clarified semantics in llvm#172012 .
Jason-Van-Beusekom pushed a commit to Jason-Van-Beusekom/llvm-project that referenced this pull request Mar 3, 2026
…lvm#184125)

This reverts commit ea3fdc5.

Re-enable const-folding for maxnum/minnum in the middle-end, GlobalISel,
and SelectionDAG.

Re-enable optimizations that depend on maxnum/minnum sNaN semantics in
InstCombine and DAGCombiner.

Now that maxnum(x, sNaN) is specified to non-deterministically produce
either NaN or x, these constant-foldings and optimizations are now valid
again according to the newly clarified semantics in llvm#172012 .
sahas3 pushed a commit to sahas3/llvm-project that referenced this pull request Mar 4, 2026
…lvm#184125)

This reverts commit ea3fdc5.

Re-enable const-folding for maxnum/minnum in the middle-end, GlobalISel,
and SelectionDAG.

Re-enable optimizations that depend on maxnum/minnum sNaN semantics in
InstCombine and DAGCombiner.

Now that maxnum(x, sNaN) is specified to non-deterministically produce
either NaN or x, these constant-foldings and optimizations are now valid
again according to the newly clarified semantics in llvm#172012 .
sujianIBM pushed a commit to sujianIBM/llvm-project that referenced this pull request Mar 5, 2026
…lvm#184125)

This reverts commit ea3fdc5.

Re-enable const-folding for maxnum/minnum in the middle-end, GlobalISel,
and SelectionDAG.

Re-enable optimizations that depend on maxnum/minnum sNaN semantics in
InstCombine and DAGCombiner.

Now that maxnum(x, sNaN) is specified to non-deterministically produce
either NaN or x, these constant-foldings and optimizations are now valid
again according to the newly clarified semantics in llvm#172012 .
rust-bors bot pushed a commit to rust-lang/rust that referenced this pull request Mar 15, 2026
use correct LLVM intrinsic for min/max on floats



The Rust `minnum`/`maxnum` intrinsics are documented to return the other argument when one input is an SNaN. However, the LLVM lowering we currently choose for them does not match those semantics: we lower them to `minnum`/`maxnum`, which (since llvm/llvm-project#172012) is documented to non-deterministically return the other argument or NaN when one input is an SNaN.

LLVM does have an intrinsic with the intended semantics: `minimumnum`/`maximumnum`. Let's use that instead. We can set the `nsz` flag since we treat signed zero ordering as non-deterministic.

Also rename the intrinsics to follow the IEEE 2019 naming, since that is mostly (and in particular, as far as NaN are concerned) now what we do. Also, `minimum_number` and `minimum` are less easy to mix up than `minnum` and `minimum`.

r? @nikic 
Cc @tgross35 
Fixes #149537
Fixes #151286
(The issues are only fixed when using the latest supported LLVM, but I don't think we usually track problems specific to people compiling rustc with old versions of LLVM)
github-actions bot pushed a commit to rust-lang/miri that referenced this pull request Mar 16, 2026
use correct LLVM intrinsic for min/max on floats



The Rust `minnum`/`maxnum` intrinsics are documented to return the other argument when one input is an SNaN. However, the LLVM lowering we currently choose for them does not match those semantics: we lower them to `minnum`/`maxnum`, which (since llvm/llvm-project#172012) is documented to non-deterministically return the other argument or NaN when one input is an SNaN.

LLVM does have an intrinsic with the intended semantics: `minimumnum`/`maximumnum`. Let's use that instead. We can set the `nsz` flag since we treat signed zero ordering as non-deterministic.

Also rename the intrinsics to follow the IEEE 2019 naming, since that is mostly (and in particular, as far as NaN are concerned) now what we do. Also, `minimum_number` and `minimum` are less easy to mix up than `minnum` and `minimum`.

r? @nikic 
Cc @tgross35 
Fixes rust-lang/rust#149537
Fixes rust-lang/rust#151286
(The issues are only fixed when using the latest supported LLVM, but I don't think we usually track problems specific to people compiling rustc with old versions of LLVM)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

floating-point Floating-point math llvm:ir

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants