-
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 #33560
base: main
Are you sure you want to change the base?
Conversation
@stephentyrone, have you had a chance to look at this? Is there anything I can do to make it easier to review? |
// If section numbers used 64 bits, then for ranges like `-1.0...1.0`, the | ||
// `Int64.random(in:using:)` call in the general case would need to call | ||
// `next()` twice on average. Each bit smaller than that halves the | ||
// probability of a second `next()` call. | ||
// | ||
// The tradeoff is wider sections, which means an increased probability of | ||
// landing in a section which spans more than one representable value and | ||
// thus requires a second random integer. | ||
// | ||
// We optimize for `Double` by using 60 bits. This gives worst-case ranges | ||
// like `-1.0...64.0` a 3% chance of needing a second random integer. | ||
@_transparent | ||
@_alwaysEmitIntoClient | ||
internal static var _sectionBitCount: Int { UInt64.bitWidth - 4 } |
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.
This will be unnecessary after #39143 “An optimal algorithm for bounded random integers” lands.
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.
May still be beneficial, let's measure; I have some further ideas for improving floating-point generation as well, though =)
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.
I look forward to hearing your ideas!
I think @goualard-f is an authority in this stuff after his survey of random-float-intervals in programming languages. I am not sure about how to best contact him -- maybe this mention will do. |
Thanks for the link. I skimmed through the article—will have to go back and read it more thoroughly later—but my initial impression is that its proposed algorithm has the goal of selecting equally spaced values in an interval. My understanding of the proposed and documented semantics for Swift’s floating-point random numbers, is that the goal here is to behave as if a real number was chosen uniformly within the interval, then rounded to a representable value. These are distinct operations, and my PR here achieves the latter. |
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 the next representable value larger thany
(or in case of overflow, then where that value would be as a real number), generating a random value in the new range, and rounding down.