-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Document NaN policy #48523
Comments
In defiance of "never say never", Up to now, almost every function has been implemented with an input-NaN semantic (another name I made up, specifying that the payload of one NaN input is propagated to the output). This is also what's usually (but perhaps not always?) used by hardware-native operations. In fact, our current input-NaN semantic is usually contingent on hardware input-NaN semantics. There is a risk that this results in re-implementations of functions that "break" existing behaviors if somebody really did rely on payload propagation. For example, I believe there is a faster Is the proposal to document this centrally or on a per-function basis? Per-function seems like it would be a never-finished job and add noise to docstrings, so I'd propose to only document it centrally. Functions which have notable deviations should be documented locally. For example, that EDIT: |
If no users care that would make things easier. I posted on slack and discourse for higher visibility. We could also run pkgeval on a branch that mangles all NaNs, but that seems like a lot of work. |
See also this IEEE standards document for some more background. The most common extant applications of NaN payloads seems to be (a) tracking exception types and (b) tracking NA (missing) values in R, neither of which are especially critical in Julia (because we normally use exceptions and |
We use an R-style tagged-NaN in SentinelArrays.jl. Specifically, this julia> Core.bitcast(Float64, typemax(UInt64))
NaN because we do a |
It seems that some people do use payload tagging in some cases. Further,
If true, this would mean that to reject payload propagation semantics would be to violate IEEE754 semantics on any function defined therein. I'm not excited about this prospect. Let's talk cost/benefit. Are there functions that we would implement differently with loosened NaN semantics? I mentioned a small optimization of It seems that, if anything, we might have to document a general policy (although perhaps not a strict guarantee) that a NaN output resulting from one or more NaN inputs should include the payload of one of the NaN inputs. We'd have to adhere to this policy for IEEE754 functions but probably should in other cases as well. |
It seems like for any function that returns NaN when one of the inputs is NaN, we can try to return the NaN that was passed in. That's how hardware float operations work, so it often happens naturally. In places where we "generate" a NaN, we should produce the "standard NaN", namely the one you get when you evaluate |
Somewhat related food for thought. Propagation of
The code below demonstrates some of the heterogeneity. x = reinterpret(Float64, reinterpret(UInt64, NaN) | 0xff);
for f in (sin, cos, tan, acos, asin, atan, log, exp, sqrt, abs2)
println(f, "\t:\t", bitstring(f(x)))
end If we want to follow Stefan's logic, then all occurrences which amount to |
That |
I concur on leaving |
How would y'all feel about the proposal in the OP: document returned payload as undefined |
the word undefined is a little scary because people think c UB, but documenting as not stable between versions would be great |
C standards would call that unspecified behavior |
I think it would be fine to document it as not something that can be relied on, but still try to return the first NaN argument when possible. We can try to do that and decide later if it's worth it. |
I disagree. I would say "one of the NaN arguments when possible." Anything more than that is going to be untenable. For example, the following two operations are implemented using a single native x86 instructions yet don't return the same positional operand when given two NaNs: julia> x = reinterpret(Float64,-1); y = reinterpret(Float64,-2);
julia> reinterpret(Int, x+y) # vaddsd
-1
julia> reinterpret(Int, ifelse(x<y,x,y)) # vminsd
-2 Hardware does not take strong positions on this so supporting any positional preference would be a pain even on a single architecture (to say nothing of multiple). Plus, compilers are free to fiddle with some operations (e.g., |
Yep, good point. One of the NaN arguments should be what we try to do. |
I feel I should note, to my great annoyance, that payload propagation is a should and not a shall according to IEEE754. I think it is still wise to try to attempt it because it simplifies reasoning about a rather... quirky condition in a type. In particular, it means that you know exactly what the value is when you do a binary operation of any NaN and any non-NaN (generally: the NaN). |
There are a bunch of different NaN values.
reinterpret(Float64, reinterpret(UInt64, NaN) + 1)
andNaN
are two examples.NaNs propagate through floating point operations.
sin(NaN)
must beNaN
in the sense ofisnan(sin(NaN))
, but which NaN should it return? Must it return the canonicalNaN
? Must it return its input? May it return some other number thatisnan
for performance reasons? These questions come up for most math functions,min
/max
/sort
, and possibly others.I propose to explicitly document that mathematical functions (e.g.
sin
,hypot
,min
) will produce an NaN result on NaN input but that which NaN is produced is an implementation detail.The text was updated successfully, but these errors were encountered: