Skip to content

Make signed zero treatment deterministic in min and max (and clamp) #154061

@RalfJung

Description

@RalfJung

Currently, the behavior of min and max (and clamp) is non-deterministic when signed zeros are involved. Non-determinism can be a significant footgun when it is unexpected as it often (and in particular in this case) means that behavior depends on the target and/or optimization level.

So, it seem worth considering changing that. In the words of @jyknight

I would strongly suggest that Rust should tweak the specification of these functions to eliminate the non-determinism in the sign of zero. IMHO, that was a historical mistake in C's fmin/fmax which nobody else should be copying.

The sign of zero is important for various mathematical floating-point algorithms, and having non-determinism in its sign, especially from a fundamental operation, is just weird. I don't think it can be justified other than compatibility with existing implementations. Even the C standard says these days, albeit in a non-binding footnote, "If possible, fmax is sensitive to the sign of zero, for example fmax(−0.0, +0.0) ideally returns +0."

and

The reason why the sign of zero is important, generally, in IEEE floating-point math is that the values are not Real numbers. So, -0 may represent "a value close to but less than zero", vs +0 may represent "a value close to but greater than zero". The distinction is useful, then, for operations that are discontiguous at zero, such as f(x) = 1/x, where in floating-point math,f(0) = Inf and f(-0) = -Inf. [...]

It's even more important in complex math, when dealing with branch cuts for a multi-valued operation such as sqrt. It turns out to be rather irritating if Complex::new(-4.0, -5.0e-324).sqrt(); ends up in a different quadrant of the complex plane than Complex::new(-4.0, -5.0e-324 / 2.0).sqrt(); (which is what would happen if the division in the latter rounded to 0.0 instead of -0.0). The widely-cited paper on all of this is William Kahan's "Branch cuts for complex elementary functions OR Much Ado About Nothing's Sign Bit".

The main reason why the treatment of signed zeros is left non-deterministic is to improve codegen on x86, which has no native support for float min/max (except for recent extensions that aren't sufficiently widely available yet). My understanding is that guaranteeing -0.0 < +0.0 for the purpose of min/max would require a few extra instructions compared to the current lowering, and t-libs-api considers that cost too high (but I haven't seen that cost actually measured). Personally, I think we should generally err on the side of defaulting to predictable portable behavior, but the performance hit of that choice needs to be tolerable (and the threshold for that is obviously subjective).

So... I think to make a case towards t-libs-api that this behavior is worth changing, someone needs to do some hacking and benchmarking to quantify the actual performance loss that reliable signed zero handling would incur on x86. I don't plan to do any follow-up work here myself, this is far outside my expertise anyway. I leave this issue as a place to track any further efforts people might want to undertake in tweaking this particular aspect of Rust. :)

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions