-
Notifications
You must be signed in to change notification settings - Fork 10.4k
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
[stdlib] Floating-point random-number improvements #33455
Conversation
Syncing from upstream
The `random(in:)` family of methods are now located in FloatingPointRandom.swift
It will reside in FloatingPoint.swift
One trick that people have been doing is submitting the benchmark changes in a separate pr because if it’s in this pr, we can’t test it against the old implementation. |
I wouldn't describe that as a trick so much as What You Should Do. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the work here!
I think this PR needs breaking up in a few ways.
- The benchmarks should be landed first so we can compare performance
- Reorganization like moving functions around should be landed as an NFC PR rather than done together, particularly to ease reviewing preservation of ABI stability.
- WIP commits should be squashed together, so separate commits should group logical changes rather than just when the work was done.
- If there are new tests that fail on the current compiler, consider putting them in as XFAILs and then un-xfailing them when the fix lands.
Benchmarks for `0..<1` are now at [https://github.com/apple/swift/pull/33462](#33462)
I've not reviewed the code itself (I'll leave that to @stephentyrone) but the quantity of new code to be inlined into the caller gives me a little bit of pause. Maybe it's not materially different though, hopefully the benchmarks will give us an idea. |
Okay, I created #33462 with just the benchmark change.
Okay, I created #33463 which just moves the existing
I wish I knew how to do this.
Yes, the last 3 tests ( |
|
Have you tried using @_specialize as an alternative to inlining? |
My understanding (as Ben explained to me on the forums) is that While |
Oh. I'm not an ABI expert, but my understanding was that something like: extension BinaryFloatingPoint {
func random(...) -> Self {
if #available(iOS 11, macOS ...) {
return newRandom()
} else {
return oldRandom()
}
}
@available(iOS 11, macOS ...)
func newRandom() -> Self {
// ...
}
} Should perform the check at runtime (as long as your deployment target allows OSes which don't pass the check, otherwise it just gets removed). As for |
At the standard library, we definitely need to serve the needs of custom numeric types (like those you might find in |
Now that #33463 has been merged (creating the file FloatingPointRandom.swift with the existing @airspeedswift will be happy to know the new PR has only 2 commits: one to update the implementation, and one to add the tests. The new benchmarks remain in #33462, which has not yet been merged at the time of this writing. |
Closed as superseded by #33560 |
Overview
This patch resolves multiple issues with generating random floating-point numbers.
The existing
random
methods onBinaryFloatingPoint
will crash for some valid ranges (SR-8798), cannot produce all values in some ranges (SR-12765), and do not follow the proposed and documented semantics. This patch solves these problems:Summary of changes
i) Finite ranges where the distance between the bounds exceeds
greatestFiniteMagnitude
previously caused a trap at run-time. Now (with this patch) they are handled correctly to produce a uniform value in the range.ii) Generating a random floating-point value in
-1..<1
left the low-bit always 0. If the magnitude was less than 1/2, then the lowest 2 bits would be 0. If less than 1/4, 3 bits, and so forth. Other ranges which spanned multiple binades had similar issues. Now all values in the input range are produced with correct probability.iii) The proposal (SE-0202: Random Unification) which added random number generation to Swift was quite sparse in its mention of floating-point semantics. However, during the review thread, discussion about the intended semantics arose with comments like this:
To which the author of the proposal responded:
Concordantly, the documentation comments for the floating-point
random
methods state:However, prior to this patch, the implementation did not match that behavior, and many representable values could not be produced in certain ranges.
Mathematical details
In order to achieve the desired semantics, it is necessary to define precisely what “converts that value to the nearest representable value” should mean. This patch takes the following axiomatic approach:
Range
Single-value ranges:
random(in: x ..< x.nextUp)
always producesx
.Adjacent intervals: If
x < y < z
, thenrandom(in: x ..< z)
is equivalent to generatingrandom(in: x ..< y
) with probability(y-x)/(z-x)
, and otherwiserandom(in: y ..< z)
with the remaining probability(z-y)/(z-x)
.In order to satisfy these two principles,
random(in: x ..< y)
must behave as if a real number r were generated uniformly in [x, y), then rounded down to the nearest representable value. Note that the rounding must be downward, as any other choice would violate one of the two principles.ClosedRange
Subintervals: If
x <= y < z
, then repeatedly generatingrandom(in: x ..< z)
until the result lands inx ... y
is equivalent to generatingrandom(in: x ... y)
.This rule ensures consistency of results produced by the
Range
andClosedRange
versions ofrandom(in:)
. As a result, it also guarantees that partitioning a closed interval into disjoint closed subintervals is consistent as well.In order to satisfy this principle,
random(in: x ... y)
must be equivalent torandom(in: x ..< y.nextUp)
if the latter is finite.In the edge-case that
y == .greatestFiniteMagnitude
, we utilize the adjacent intervals principle on [x, y) and [y, y + y.ulp). Although the latter endpoint is not representable as a finite floating-point value, the conceptual idea still holds, and the probability of producingy
is proportional toy.ulp
just as it is for all other values in the same binade.This patch implements those semantics.
Similarity with random integer methods
It is interesting to note that the strategy of generating a uniform real number in a half-open interval then rounding down, is equivalent to how the
random
methods work for integer types. That is,T.random(in: x ..< y)
behaves as if choosing a real number r uniformly in [x, y) then rounding down to the next representable value, regardless of whetherT
is an integer or (with this patch) a floating-point type.Similarly for closed ranges,
T.random(in: x ... y)
behaves as if extending to a half-open interval bounded above by either the next representable value larger thany
, or in case of overflow then where that value would as a real number, generating a random value in the new range, and rounding down.