I added the proposed methods to the existing FloatingPointGenerationBenchmark in the RNG JMH testing project. The nextDouble methods already present match the 3 variants proposed without the addition of a single trailing 1-bit. I also added two methods that will reject the value of zero either using a while loop or recursively calling the method until a non-zero is generated. The advantage of recursion is that an infinite loop will not occur due to stack memory overflow if the source RNG is broken (always outputs 0).
Here are the results using JDK 21 on a Mac M2 Pro. nextDoubleUsingBitsToDouble thrpt 10 1036.995 ± 10.052 ops/us nextDoubleUsingMultiply52bits thrpt 10 1041.926 ± 13.284 ops/us nextDoubleUsingMultiply53bits thrpt 10 1060.602 ± 20.994 ops/us nextOpenDoubleUsingBitsToDouble thrpt 10 1207.123 ± 17.814 ops/us nextOpenDoubleUsingMultiply52bits thrpt 10 991.166 ± 17.743 ops/us nextOpenDoubleUsingMultiply53bits thrpt 10 976.256 ± 13.821 ops/us nextOpenDoubleUsingRecursion thrpt 10 1151.243 ± 10.216 ops/us nextOpenDoubleUsingRejection thrpt 10 1285.009 ± 13.680 ops/us Using rejection or recursion is slower than the branchless versions with multiplication. The multiplication version is faster if there is a trailing 1-bit. This could possibly be related to floating-point multiplication with a guaranteed non-zero result. The 52 and 53 bit multiplication are close enough to be within error. The method with the conversion of long bits to double was strange. It does well for the standard case of the [0, 1) interval. IIRC that was not the case on older processors used for this benchmark in the past. It is slower for the open interval. The two methods are: Double.longBitsToDouble((source.nextLong() >>> 12) | (0x3ffL << 52)) - 1.0; Double.longBitsToDouble((source.nextLong() >>> 12) | 0x3ff0000000000001L) - 1.0 If the former is changed to the following it is slower: Double.longBitsToDouble((source.nextLong() >>> 12) | 0x3ff0000000000000L) - 1.0 nextDoubleUsingBitsToDouble thrpt 10 1200.327 ± 24.800 ops/us Loading the long constant is slower than generating it using a shift. These results are for a single JVM and processor. However it does put a case forward for a branchless version of the ContinuousUniformSampler when the open interval is requested to be in (0, 1). I can raise a ticket for this in Jira to record the benchmark results and document the potential change. Alex On Thu, 12 Feb 2026 at 16:47, Alex Herbert <[email protected]> wrote: > Hi, > > The code in Commons RNG provides a general interface for generating > primitive values in UniformRandomProvider [1]. This closely matches the > JDK's own interface in RandomGenerator (Java 17+) [2]. Although it is > possible to add more methods to UniformRandomProvider this risks > cluttering the interface with specialist methods that may not be > commonly used. If it's not in the JDK's interface then typically we would > not support it. > > The interfaces are mostly the same. Differences are: > > UniformRandomProvider: > void nextBytes(byte[] bytes, int start, int len) > > RandomGenerator: > double nextExponential() > double nextGaussian() > > If you wish to sample from a exponential or Gaussian then we have samplers > in the sampling module. These include the same sampling method used in the > JDK which is based on McFarland's modification of a ziggurat algorithm by > Marsaglia. > > If you wish to sample from an open interval then we have > the ContinuousUniformSampler [3] that samples within [lo, hi) by default > but can be changed to an open interval of (lo, hi) with a constructor > argument. Since the range can use any double values this requires some > floating-point computations to map a generated [0, 1) to the interval [lo, > hi), or (lo, hi). Since rounding can occur you can see values at the bounds > even when the original double was non-zero. So a rejection algorithm is > used: that is if sample == lo or sample == hi then repeat. Rejection > frequency is small unless the range between lo and hi does not contain many > floating-point values. Thus this rejection is efficiently ignored due to > branch prediction. > > Note that the constructor for this sampler validates there are values > between lo and hi. Otherwise you can have an infinite loop. Thus it > supports generation of open intervals with bounds 2 ULP or more apart, or 3 > if the bounds span zero to account for -0.0. > > If you specifically require a value in (0, 1) we could add a specialised > version to this sampler to use a faster computation. But the user must be > warned that multiplication of (0, 1) by a floating point range can result > in a semi-open interval result due to rounding. For example the smallest > dyadic rational in 0-1 is 2^-53. Use this to sample from the range (2, 4): > > jshell > | Welcome to JShell -- Version 21.0.9 > | For an introduction type: /help intro > > jshell> 0x1.0p-53 * (4 - 2) + 2 > $1 ==> 2.0 > > This can be avoided using: > > UniformRandomProvider rng = ... > double x = ContinuousUniformSampler.of(rng, 2, 4, true).sample(); > > If the ultimate requirement is float values in the range (0, 1) then a > faster algorithm is possible. But this is not always what the user wants > and we should document the possible pitfalls as described above. > > Alex > > [1] > https://commons.apache.org/proper/commons-rng/commons-rng-docs/apidocs/org/apache/commons/rng/UniformRandomProvider.html > [2] > https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/random/RandomGenerator.html > [3] > https://commons.apache.org/proper/commons-rng/commons-rng-docs/apidocs/org/apache/commons/rng/sampling/distribution/ContinuousUniformSampler.html > > > On Thu, 12 Feb 2026 at 13:33, Gilles Sadowski <[email protected]> > wrote: > >> Hello. >> >> Le jeu. 12 févr. 2026 à 13:54, Jherek Healy >> <[email protected]> a écrit : >> > >> > Dear Commons RNG Team, >> > >> > I am proposing to introduce a new method in IntProvider and >> UniformRandomProvider which computes a random double number in the open >> interval (0, 1). >> > >> > Right now, nextDouble() computes a random double in the semi-closed >> interval [0,1). This can be problematic when the random number is to be >> used in a inverse distribution function, to provide random numbers >> according to a specific distribution, as the inverse distribution function >> is only defined on the open interval. >> >> Is this the sole use-case? >> If so, wouldn't it be better (design-wise) to implement the functionality >> in the "o.a.c.rng.sampling.distribution" package? >> >> Regards, >> Gilles >> >> [1] >> https://commons.apache.org/proper/commons-rng/commons-rng-sampling/index.html >> >> > The idea is to match the implementation of >> https://www.math.sci.hiroshima-u.ac.jp/m-mat/MT/VERSIONS/C-LANG/mt19937-64.cgenrand64_real3 >> > >> > double genrand64_real3(void) >> > { >> > return ((genrand64_int64() >> 12) + 0.5) * (1.0/4503599627370496.0); >> > } >> > >> > There are two possible Java implementations: >> > ((nextLong() >>> 12) + 0.5) * 0x1.0p-52; >> > or equivalently (reusing the constant used for the semi-closed interval) >> > ((v >>> 11) | 1) * * 0x1.0p-53; >> > >> > Yet another alternvative (which produces different numbers (last >> digit)) is the union trick: >> > long bits = (random64 >>> 12) | 0x3FF0000000000001L; >> > return Double.longBitsToDouble(bits) - 1.0; >> > >> > I don't have a strong preference in either of the choices. >> > >> > Jherek >> >> --------------------------------------------------------------------- >> To unsubscribe, e-mail: [email protected] >> For additional commands, e-mail: [email protected] >> >>
