From ecda959b991a9a66c66d01d4d92c3b07dca028e4 Mon Sep 17 00:00:00 2001 From: Russ Cox Date: Tue, 6 Jun 2023 10:49:19 -0400 Subject: [PATCH] math/rand/v2: optimize Float32, Float64 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We realized too late after Go 1 that float64(r.Uint64())/(1<<64) is not a correct implementation: it occasionally rounds to 1. The correct implementation is float64(r.Uint64()&(1<<53-1))/(1<<53) but we couldn't change the implementation for compatibility, so we changed it to retry only in the "round to 1" cases. The change to v2 lets us update the algorithm to the simpler, faster one. Note that this implementation cannot generate 2⁻⁵⁴, nor 2⁻¹⁰⁰, nor any of the other numbers between 0 and 2⁻⁵³. A slower algorithm could shift some of the probability of generating these two boundary values over to the values in between, but that would be much slower and not necessarily be better. In particular, the current implementation has the property that there are uniform gaps between the possible returned floats, which might help stability. Also, the result is often scaled and shifted, like Float64()*X+Y. Multiplying by X>1 would open new gaps, and adding most Y would erase all the distinctions that were introduced. The only changes to benchmarks should be in Float32 and Float64. The other changes remain a cautionary tale. goos: linux goarch: amd64 pkg: math/rand/v2 cpu: AMD Ryzen 9 7950X 16-Core Processor │ 4d84a369d1.amd64 │ 2703446c2e.amd64 │ │ sec/op │ sec/op vs base │ SourceUint64-32 1.348n ± 2% 1.337n ± 1% ~ (p=0.662 n=20) GlobalInt64-32 2.082n ± 2% 2.225n ± 2% +6.87% (p=0.000 n=20) GlobalInt64Parallel-32 0.1036n ± 1% 0.1043n ± 2% ~ (p=0.171 n=20) GlobalUint64-32 2.077n ± 2% 2.058n ± 1% ~ (p=0.560 n=20) GlobalUint64Parallel-32 0.1012n ± 1% 0.1009n ± 1% ~ (p=0.995 n=20) Int64-32 1.750n ± 0% 1.719n ± 2% -1.74% (p=0.000 n=20) Uint64-32 1.707n ± 2% 1.669n ± 1% -2.20% (p=0.000 n=20) GlobalIntN1000-32 3.192n ± 1% 3.321n ± 2% +4.04% (p=0.000 n=20) IntN1000-32 2.462n ± 2% 2.479n ± 1% ~ (p=0.417 n=20) Int64N1000-32 2.470n ± 1% 2.477n ± 1% ~ (p=0.664 n=20) Int64N1e8-32 2.503n ± 2% 2.490n ± 1% ~ (p=0.245 n=20) Int64N1e9-32 2.487n ± 1% 2.458n ± 1% ~ (p=0.032 n=20) Int64N2e9-32 2.487n ± 1% 2.486n ± 2% ~ (p=0.507 n=20) Int64N1e18-32 3.006n ± 2% 3.215n ± 2% +6.94% (p=0.000 n=20) Int64N2e18-32 3.368n ± 1% 3.588n ± 2% +6.55% (p=0.000 n=20) Int64N4e18-32 4.763n ± 1% 4.938n ± 2% +3.69% (p=0.000 n=20) Int32N1000-32 2.403n ± 1% 2.673n ± 2% +11.19% (p=0.000 n=20) Int32N1e8-32 2.405n ± 1% 2.631n ± 2% +9.42% (p=0.000 n=20) Int32N1e9-32 2.402n ± 2% 2.628n ± 2% +9.41% (p=0.000 n=20) Int32N2e9-32 2.384n ± 1% 2.684n ± 2% +12.56% (p=0.000 n=20) Float32-32 2.641n ± 2% 2.240n ± 2% -15.18% (p=0.000 n=20) Float64-32 2.483n ± 1% 2.253n ± 1% -9.26% (p=0.000 n=20) ExpFloat64-32 3.486n ± 2% 3.677n ± 1% +5.49% (p=0.000 n=20) NormFloat64-32 3.648n ± 1% 3.761n ± 1% +3.11% (p=0.000 n=20) Perm3-32 33.04n ± 1% 33.55n ± 2% ~ (p=0.180 n=20) Perm30-32 171.9n ± 1% 173.2n ± 1% ~ (p=0.050 n=20) Perm30ViaShuffle-32 100.3n ± 1% 115.9n ± 1% +15.55% (p=0.000 n=20) ShuffleOverhead-32 102.5n ± 1% 101.9n ± 1% ~ (p=0.266 n=20) Concurrent-32 2.101n ± 0% 2.107n ± 6% ~ (p=0.212 n=20) goos: darwin goarch: arm64 pkg: math/rand/v2 cpu: Apple M1 │ 4d84a369d1.arm64 │ 2703446c2e.arm64 │ │ sec/op │ sec/op vs base │ SourceUint64-8 2.261n ± 1% 2.275n ± 0% ~ (p=0.082 n=20) GlobalInt64-8 2.160n ± 1% 2.154n ± 1% ~ (p=0.490 n=20) GlobalInt64Parallel-8 0.4299n ± 0% 0.4298n ± 0% ~ (p=0.663 n=20) GlobalUint64-8 2.169n ± 1% 2.160n ± 1% ~ (p=0.292 n=20) GlobalUint64Parallel-8 0.4293n ± 1% 0.4286n ± 0% ~ (p=0.155 n=20) Int64-8 2.473n ± 1% 2.491n ± 1% ~ (p=0.317 n=20) Uint64-8 2.453n ± 1% 2.458n ± 0% ~ (p=0.941 n=20) GlobalIntN1000-8 2.814n ± 2% 2.814n ± 2% ~ (p=0.972 n=20) IntN1000-8 2.933n ± 2% 2.933n ± 0% ~ (p=0.287 n=20) Int64N1000-8 2.934n ± 2% 2.962n ± 1% ~ (p=0.062 n=20) Int64N1e8-8 2.935n ± 2% 2.960n ± 1% ~ (p=0.183 n=20) Int64N1e9-8 2.934n ± 2% 2.935n ± 2% ~ (p=0.367 n=20) Int64N2e9-8 2.935n ± 2% 2.934n ± 0% ~ (p=0.455 n=20) Int64N1e18-8 3.778n ± 1% 3.777n ± 1% ~ (p=0.995 n=20) Int64N2e18-8 4.359n ± 1% 4.359n ± 1% ~ (p=0.122 n=20) Int64N4e18-8 6.546n ± 1% 6.536n ± 1% ~ (p=0.920 n=20) Int32N1000-8 2.940n ± 2% 2.937n ± 0% ~ (p=0.149 n=20) Int32N1e8-8 2.937n ± 2% 2.937n ± 1% ~ (p=0.620 n=20) Int32N1e9-8 2.938n ± 0% 2.936n ± 0% ~ (p=0.046 n=20) Int32N2e9-8 2.938n ± 2% 2.938n ± 2% ~ (p=0.455 n=20) Float32-8 3.486n ± 0% 3.441n ± 0% -1.28% (p=0.000 n=20) Float64-8 3.480n ± 0% 3.441n ± 0% -1.13% (p=0.000 n=20) ExpFloat64-8 4.533n ± 0% 4.486n ± 0% -1.03% (p=0.000 n=20) NormFloat64-8 4.764n ± 0% 4.721n ± 0% -0.90% (p=0.000 n=20) Perm3-8 26.66n ± 0% 26.65n ± 0% ~ (p=0.019 n=20) Perm30-8 143.4n ± 0% 143.2n ± 0% -0.17% (p=0.000 n=20) Perm30ViaShuffle-8 142.9n ± 0% 143.0n ± 0% ~ (p=0.522 n=20) ShuffleOverhead-8 120.7n ± 0% 120.6n ± 1% ~ (p=0.488 n=20) Concurrent-8 2.360n ± 2% 2.399n ± 5% ~ (p=0.062 n=20) goos: linux goarch: 386 pkg: math/rand/v2 cpu: AMD Ryzen 9 7950X 16-Core Processor │ 4d84a369d1.386 │ 2703446c2e.386 │ │ sec/op │ sec/op vs base │ SourceUint64-32 2.101n ± 2% 2.072n ± 2% ~ (p=0.273 n=20) GlobalInt64-32 3.518n ± 2% 3.546n ± 27% +0.78% (p=0.007 n=20) GlobalInt64Parallel-32 0.3206n ± 0% 0.3211n ± 0% ~ (p=0.386 n=20) GlobalUint64-32 3.538n ± 1% 3.522n ± 2% ~ (p=0.331 n=20) GlobalUint64Parallel-32 0.3231n ± 0% 0.3172n ± 0% -1.84% (p=0.000 n=20) Int64-32 2.554n ± 2% 2.520n ± 2% ~ (p=0.465 n=20) Uint64-32 2.575n ± 2% 2.581n ± 1% ~ (p=0.213 n=20) GlobalIntN1000-32 6.292n ± 1% 6.171n ± 1% ~ (p=0.015 n=20) IntN1000-32 4.735n ± 1% 4.752n ± 2% ~ (p=0.635 n=20) Int64N1000-32 5.489n ± 2% 5.429n ± 1% ~ (p=0.324 n=20) Int64N1e8-32 5.528n ± 2% 5.469n ± 2% ~ (p=0.013 n=20) Int64N1e9-32 5.438n ± 2% 5.489n ± 2% ~ (p=0.984 n=20) Int64N2e9-32 5.474n ± 1% 5.492n ± 2% ~ (p=0.616 n=20) Int64N1e18-32 9.053n ± 1% 8.927n ± 1% ~ (p=0.037 n=20) Int64N2e18-32 9.685n ± 2% 9.622n ± 1% ~ (p=0.449 n=20) Int64N4e18-32 12.18n ± 1% 12.03n ± 1% ~ (p=0.013 n=20) Int32N1000-32 4.862n ± 1% 4.817n ± 1% -0.94% (p=0.002 n=20) Int32N1e8-32 4.758n ± 2% 4.801n ± 1% ~ (p=0.597 n=20) Int32N1e9-32 4.772n ± 1% 4.798n ± 1% ~ (p=0.774 n=20) Int32N2e9-32 4.847n ± 0% 4.840n ± 1% ~ (p=0.867 n=20) Float32-32 22.18n ± 4% 10.51n ± 4% -52.61% (p=0.000 n=20) Float64-32 21.21n ± 3% 20.33n ± 3% -4.17% (p=0.000 n=20) ExpFloat64-32 12.39n ± 2% 12.59n ± 2% ~ (p=0.139 n=20) NormFloat64-32 7.422n ± 1% 7.350n ± 2% ~ (p=0.208 n=20) Perm3-32 38.00n ± 2% 39.29n ± 2% +3.38% (p=0.000 n=20) Perm30-32 212.7n ± 1% 219.1n ± 2% +3.03% (p=0.001 n=20) Perm30ViaShuffle-32 187.5n ± 2% 189.8n ± 2% ~ (p=0.457 n=20) ShuffleOverhead-32 159.7n ± 1% 158.9n ± 2% ~ (p=0.920 n=20) Concurrent-32 3.470n ± 0% 3.306n ± 3% -4.71% (p=0.000 n=20) For #61716. Change-Id: I1933f1f9efd7e6e832d83e7fa5d84398f67d41f5 Reviewed-on: https://go-review.googlesource.com/c/go/+/502503 Auto-Submit: Russ Cox Reviewed-by: Rob Pike Reviewed-by: Dmitri Shuralyov LUCI-TryBot-Result: Go LUCI --- src/math/rand/v2/example_test.go | 4 +- src/math/rand/v2/rand.go | 35 ++------------ src/math/rand/v2/regress_test.go | 82 ++++++++++++++++---------------- 3 files changed, 47 insertions(+), 74 deletions(-) diff --git a/src/math/rand/v2/example_test.go b/src/math/rand/v2/example_test.go index 762867443910a8..590e01d1bb87eb 100644 --- a/src/math/rand/v2/example_test.go +++ b/src/math/rand/v2/example_test.go @@ -83,8 +83,8 @@ func Example_rand() { // Perm generates a random permutation of the numbers [0, n). show("Perm", r.Perm(5), r.Perm(5), r.Perm(5)) // Output: - // Float32 0.2635776 0.6358173 0.6718283 - // Float64 0.628605430454327 0.4504798828572669 0.9562755949377957 + // Float32 0.73793465 0.38461488 0.9940225 + // Float64 0.6919607852308565 0.29140004584133117 0.2262092163027547 // ExpFloat64 0.10400903165715357 0.28855743344575835 0.20489656480442942 // NormFloat64 -0.5602299711828513 -0.9211692958208376 -1.4262061075859056 // Int32 1817075958 91420417 1486590581 diff --git a/src/math/rand/v2/rand.go b/src/math/rand/v2/rand.go index 3b8d244154cb07..7e8be1ac4f23b2 100644 --- a/src/math/rand/v2/rand.go +++ b/src/math/rand/v2/rand.go @@ -217,41 +217,14 @@ func (r *Rand) UintN(n uint) uint { // Float64 returns, as a float64, a pseudo-random number in the half-open interval [0.0,1.0). func (r *Rand) Float64() float64 { - // A clearer, simpler implementation would be: - // return float64(r.Int64N(1<<53)) / (1<<53) - // However, Go 1 shipped with - // return float64(r.Int64()) / (1 << 63) - // and we want to preserve that value stream. - // - // There is one bug in the value stream: r.Int64() may be so close - // to 1<<63 that the division rounds up to 1.0, and we've guaranteed - // that the result is always less than 1.0. - // - // We tried to fix this by mapping 1.0 back to 0.0, but since float64 - // values near 0 are much denser than near 1, mapping 1 to 0 caused - // a theoretically significant overshoot in the probability of returning 0. - // Instead of that, if we round up to 1, just try again. - // Getting 1 only happens 1/2⁵³ of the time, so most clients - // will not observe it anyway. -again: - f := float64(r.Int64()) / (1 << 63) - if f == 1 { - goto again // resample; this branch is taken O(never) - } - return f + // There are exactly 1<<53 float64s in [0,1). Use Intn(1<<53) / (1<<53). + return float64(r.Uint64()<<11>>11) / (1 << 53) } // Float32 returns, as a float32, a pseudo-random number in the half-open interval [0.0,1.0). func (r *Rand) Float32() float32 { - // Same rationale as in Float64: we want to preserve the Go 1 value - // stream except we want to fix it not to return 1.0 - // This only happens 1/2²⁴ of the time (plus the 1/2⁵³ of the time in Float64). -again: - f := float32(r.Float64()) - if f == 1 { - goto again // resample; this branch is taken O(very rarely) - } - return f + // There are exactly 1<<24 float32s in [0,1). Use Intn(1<<24) / (1<<24). + return float32(r.Uint32()<<8>>8) / (1 << 24) } // Perm returns, as a slice of n ints, a pseudo-random permutation of the integers diff --git a/src/math/rand/v2/regress_test.go b/src/math/rand/v2/regress_test.go index beefce86383206..3b886041f7a14d 100644 --- a/src/math/rand/v2/regress_test.go +++ b/src/math/rand/v2/regress_test.go @@ -246,47 +246,47 @@ var regressGolden = []any{ float64(0.22199446394172706), // ExpFloat64() float64(2.248962105270165), // ExpFloat64() - float32(0.6046603), // Float32() - float32(0.9405091), // Float32() - float32(0.6645601), // Float32() - float32(0.4377142), // Float32() - float32(0.4246375), // Float32() - float32(0.68682307), // Float32() - float32(0.06563702), // Float32() - float32(0.15651925), // Float32() - float32(0.09696952), // Float32() - float32(0.30091187), // Float32() - float32(0.51521266), // Float32() - float32(0.81363994), // Float32() - float32(0.21426387), // Float32() - float32(0.3806572), // Float32() - float32(0.31805816), // Float32() - float32(0.46888983), // Float32() - float32(0.28303415), // Float32() - float32(0.29310185), // Float32() - float32(0.67908466), // Float32() - float32(0.21855305), // Float32() - - float64(0.6046602879796196), // Float64() - float64(0.9405090880450124), // Float64() - float64(0.6645600532184904), // Float64() - float64(0.4377141871869802), // Float64() - float64(0.4246374970712657), // Float64() - float64(0.6868230728671094), // Float64() - float64(0.06563701921747622), // Float64() - float64(0.15651925473279124), // Float64() - float64(0.09696951891448456), // Float64() - float64(0.30091186058528707), // Float64() - float64(0.5152126285020654), // Float64() - float64(0.8136399609900968), // Float64() - float64(0.21426387258237492), // Float64() - float64(0.380657189299686), // Float64() - float64(0.31805817433032985), // Float64() - float64(0.4688898449024232), // Float64() - float64(0.28303415118044517), // Float64() - float64(0.29310185733681576), // Float64() - float64(0.6790846759202163), // Float64() - float64(0.21855305259276428), // Float64() + float32(0.39651686), // Float32() + float32(0.38516325), // Float32() + float32(0.06368679), // Float32() + float32(0.027415931), // Float32() + float32(0.3535996), // Float32() + float32(0.9133533), // Float32() + float32(0.40153843), // Float32() + float32(0.034464598), // Float32() + float32(0.4120984), // Float32() + float32(0.51671815), // Float32() + float32(0.9472164), // Float32() + float32(0.14591497), // Float32() + float32(0.42577565), // Float32() + float32(0.7241202), // Float32() + float32(0.7114463), // Float32() + float32(0.01790011), // Float32() + float32(0.22837132), // Float32() + float32(0.5170377), // Float32() + float32(0.9228385), // Float32() + float32(0.9747907), // Float32() + + float64(0.17213489113047786), // Float64() + float64(0.0813061580926816), // Float64() + float64(0.5094944957341486), // Float64() + float64(0.2193276794677107), // Float64() + float64(0.8287970009760902), // Float64() + float64(0.30682661592006877), // Float64() + float64(0.21230767869565503), // Float64() + float64(0.2757168463782187), // Float64() + float64(0.2967873684321951), // Float64() + float64(0.13374523933395033), // Float64() + float64(0.5777315861149934), // Float64() + float64(0.16732005385910476), // Float64() + float64(0.40620552435192425), // Float64() + float64(0.7929618428784644), // Float64() + float64(0.691570514257735), // Float64() + float64(0.14320118008134408), // Float64() + float64(0.8269708087758376), // Float64() + float64(0.13630191289931604), // Float64() + float64(0.38270814230149663), // Float64() + float64(0.7983258549906352), // Float64() int64(5577006791947779410), // Int() int64(8674665223082153551), // Int()