Skip to content

Commit 2e7508c

Browse files
jrickdavecgh
authored andcommitted
[release-v2.0] rand: Use stdlib crypto/rand.Read on OpenBSD + Go 1.24
On Go 1.24, the standard library crypto/rand.Read method will read cryptographically secure bytes using arc4random_buf(3) instead of getentropy(2). This avoids a context switch to kernel for a system call and is much faster on small reads than both the previous stdlib crypto/rand reader, and our custom implemented userspace PRNG. It also avoids the need to provide (additional) locking to the dcrd's package global userspace PRNG. goos: openbsd goarch: amd64 pkg: github.com/decred/dcrd/crypto/rand cpu: AMD Ryzen 7 5800X3D 8-Core Processor │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ DcrdRead/4b-8 149.7n ± 1% 124.3n ± 1% -16.91% (p=0.000 n=10) DcrdRead/8b-8 163.8n ± 0% 137.8n ± 1% -15.84% (p=0.000 n=10) DcrdRead/32b-8 243.9n ± 1% 232.2n ± 2% -4.82% (p=0.001 n=10) DcrdRead/512b-8 1.460µ ± 0% 1.814µ ± 0% +24.25% (p=0.000 n=10) DcrdRead/1KiB-8 2.770µ ± 0% 3.501µ ± 3% +26.39% (p=0.000 n=10) DcrdRead/4KiB-8 10.50µ ± 1% 13.55µ ± 4% +29.06% (p=0.000 n=10) StdlibRead/4b-8 519.5n ± 1% 124.1n ± 0% -76.11% (p=0.000 n=10) StdlibRead/8b-8 534.5n ± 1% 137.9n ± 1% -74.20% (p=0.000 n=10) StdlibRead/32b-8 624.3n ± 2% 231.9n ± 1% -62.86% (p=0.000 n=10) StdlibRead/512b-8 2.631µ ± 0% 1.816µ ± 0% -30.98% (p=0.000 n=10) StdlibRead/1KiB-8 5.196µ ± 0% 3.494µ ± 0% -32.76% (p=0.000 n=10) StdlibRead/4KiB-8 20.52µ ± 0% 13.52µ ± 0% -34.12% (p=0.000 n=10) DcrdReadPRNG/4b-8 140.6n ± 0% 124.1n ± 0% -11.74% (p=0.000 n=10) DcrdReadPRNG/8b-8 154.9n ± 0% 137.2n ± 1% -11.43% (p=0.000 n=10) DcrdReadPRNG/32b-8 228.8n ± 0% 232.0n ± 0% +1.42% (p=0.001 n=10) DcrdReadPRNG/512b-8 1.423µ ± 0% 1.816µ ± 0% +27.54% (p=0.000 n=10) DcrdReadPRNG/1KiB-8 2.721µ ± 0% 3.496µ ± 0% +28.49% (p=0.000 n=10) DcrdReadPRNG/4KiB-8 10.45µ ± 0% 13.52µ ± 0% +29.35% (p=0.000 n=10) Int32N-8 174.4n ± 1% 148.4n ± 0% -14.88% (p=0.000 n=10) Uint32N-8 173.6n ± 1% 147.0n ± 0% -15.32% (p=0.000 n=10) Int64N-8 170.9n ± 1% 146.1n ± 0% -14.48% (p=0.000 n=10) Uint64N-8 170.4n ± 0% 145.9n ± 1% -14.40% (p=0.000 n=10) Duration-8 191.1n ± 7% 159.0n ± 10% -16.80% (p=0.000 n=10) ShuffleSlice-8 161.5n ± 0% 144.6n ± 1% -10.50% (p=0.000 n=10) geomean 670.3n 542.5n -19.07%
1 parent 2417fe0 commit 2e7508c

File tree

5 files changed

+100
-49
lines changed

5 files changed

+100
-49
lines changed

crypto/rand/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ The default global PRNG will never panic after package init and is safe for
1919
concurrent access. Additional PRNGs which avoid the locking overhead can be
2020
created by calling `NewPRNG`.
2121

22+
On select operating systems and Go versions, this package may fallback to
23+
`crypto/rand` when it is already implemented by a fast userspace CSPRNG.
24+
2225
## Statistical Test Quality Assessment Results
2326

2427
The quality of the random number generation provided by this implementation has

crypto/rand/default.go

Lines changed: 28 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package rand
77
import (
88
"io"
99
"math/big"
10-
"sync"
1110
"time"
1211
)
1312

@@ -18,28 +17,8 @@ func Reader() io.Reader {
1817
return globalRand
1918
}
2019

21-
type lockingPRNG struct {
22-
*PRNG
23-
mu sync.Mutex
24-
}
25-
2620
var globalRand *lockingPRNG
2721

28-
func init() {
29-
p, err := NewPRNG()
30-
if err != nil {
31-
panic(err)
32-
}
33-
globalRand = &lockingPRNG{PRNG: p}
34-
}
35-
36-
func (p *lockingPRNG) Read(s []byte) (n int, err error) {
37-
p.mu.Lock()
38-
defer p.mu.Unlock()
39-
40-
return p.PRNG.Read(s)
41-
}
42-
4322
// Read fills b with random bytes obtained from the default userspace PRNG.
4423
func Read(b []byte) {
4524
// Mutex is acquired by (*lockingPRNG).Read.
@@ -48,41 +27,41 @@ func Read(b []byte) {
4827

4928
// Uint32 returns a uniform random uint32.
5029
func Uint32() uint32 {
51-
globalRand.mu.Lock()
52-
defer globalRand.mu.Unlock()
30+
globalRand.Lock()
31+
defer globalRand.Unlock()
5332

5433
return globalRand.Uint32()
5534
}
5635

5736
// Uint64 returns a uniform random uint64.
5837
func Uint64() uint64 {
59-
globalRand.mu.Lock()
60-
defer globalRand.mu.Unlock()
38+
globalRand.Lock()
39+
defer globalRand.Unlock()
6140

6241
return globalRand.Uint64()
6342
}
6443

6544
// Uint32N returns a random uint32 in range [0,n) without modulo bias.
6645
func Uint32N(n uint32) uint32 {
67-
globalRand.mu.Lock()
68-
defer globalRand.mu.Unlock()
46+
globalRand.Lock()
47+
defer globalRand.Unlock()
6948

7049
return globalRand.Uint32N(n)
7150
}
7251

7352
// Uint64N returns a random uint32 in range [0,n) without modulo bias.
7453
func Uint64N(n uint64) uint64 {
75-
globalRand.mu.Lock()
76-
defer globalRand.mu.Unlock()
54+
globalRand.Lock()
55+
defer globalRand.Unlock()
7756

7857
return globalRand.Uint64N(n)
7958
}
8059

8160
// Int32 returns a random 31-bit non-negative integer as an int32 without
8261
// modulo bias.
8362
func Int32() int32 {
84-
globalRand.mu.Lock()
85-
defer globalRand.mu.Unlock()
63+
globalRand.Lock()
64+
defer globalRand.Unlock()
8665

8766
return globalRand.Int32()
8867
}
@@ -91,17 +70,17 @@ func Int32() int32 {
9170
// without modulo bias.
9271
// Panics if n <= 0.
9372
func Int32N(n int32) int32 {
94-
globalRand.mu.Lock()
95-
defer globalRand.mu.Unlock()
73+
globalRand.Lock()
74+
defer globalRand.Unlock()
9675

9776
return globalRand.Int32N(n)
9877
}
9978

10079
// Int64 returns a random 63-bit non-negative integer as an int64 without
10180
// modulo bias.
10281
func Int64() int64 {
103-
globalRand.mu.Lock()
104-
defer globalRand.mu.Unlock()
82+
globalRand.Lock()
83+
defer globalRand.Unlock()
10584

10685
return globalRand.Int64()
10786
}
@@ -110,16 +89,16 @@ func Int64() int64 {
11089
// without modulo bias.
11190
// Panics if n <= 0.
11291
func Int64N(n int64) int64 {
113-
globalRand.mu.Lock()
114-
defer globalRand.mu.Unlock()
92+
globalRand.Lock()
93+
defer globalRand.Unlock()
11594

11695
return globalRand.Int64N(n)
11796
}
11897

11998
// Int returns a non-negative integer without bias.
12099
func Int() int {
121-
globalRand.mu.Lock()
122-
defer globalRand.mu.Unlock()
100+
globalRand.Lock()
101+
defer globalRand.Unlock()
123102

124103
return globalRand.Int()
125104
}
@@ -128,25 +107,25 @@ func Int() int {
128107
// modulo bias.
129108
// Panics if n <= 0.
130109
func IntN(n int) int {
131-
globalRand.mu.Lock()
132-
defer globalRand.mu.Unlock()
110+
globalRand.Lock()
111+
defer globalRand.Unlock()
133112

134113
return globalRand.IntN(n)
135114
}
136115

137116
// UintN returns, as an uint, a random integer in [0,n) without modulo bias.
138117
func UintN(n uint) uint {
139-
globalRand.mu.Lock()
140-
defer globalRand.mu.Unlock()
118+
globalRand.Lock()
119+
defer globalRand.Unlock()
141120

142121
return globalRand.UintN(n)
143122
}
144123

145124
// Duration returns a random duration in [0,n) without modulo bias.
146125
// Panics if n <= 0.
147126
func Duration(n time.Duration) time.Duration {
148-
globalRand.mu.Lock()
149-
defer globalRand.mu.Unlock()
127+
globalRand.Lock()
128+
defer globalRand.Unlock()
150129

151130
return globalRand.Duration(n)
152131
}
@@ -155,8 +134,8 @@ func Duration(n time.Duration) time.Duration {
155134
// indexes i and j.
156135
// Panics if n < 0.
157136
func Shuffle(n int, swap func(i, j int)) {
158-
globalRand.mu.Lock()
159-
defer globalRand.mu.Unlock()
137+
globalRand.Lock()
138+
defer globalRand.Unlock()
160139

161140
globalRand.Shuffle(n, swap)
162141
}
@@ -171,8 +150,8 @@ func ShuffleSlice[S ~[]E, E any](s S) {
171150
// Int returns a uniform random value in [0,max).
172151
// Panics if max <= 0.
173152
func BigInt(max *big.Int) *big.Int {
174-
globalRand.mu.Lock()
175-
defer globalRand.mu.Unlock()
153+
globalRand.Lock()
154+
defer globalRand.Unlock()
176155

177156
return globalRand.PRNG.BigInt(max)
178157
}

crypto/rand/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@
1010
// The default global PRNG will never panic after package init and is safe for
1111
// concurrent access. Additional PRNGs which avoid the locking overhead can
1212
// be created by calling NewPRNG.
13+
//
14+
// On select operating systems and Go versions, this package may fallback to
15+
// crypto/rand when it is already implemented by a fast userspace CSPRNG.
1316
package rand

crypto/rand/prng.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
// Use of this source code is governed by an ISC
33
// license that can be found in the LICENSE file.
44

5+
//go:build !(openbsd && go1.24)
6+
57
package rand
68

79
import (
810
cryptorand "crypto/rand"
911
"encoding/binary"
1012
"math/bits"
13+
"sync"
1114
"time"
1215

1316
"golang.org/x/crypto/chacha20"
@@ -104,3 +107,23 @@ func (p *PRNG) Read(s []byte) (n int, err error) {
104107
n += len(s)
105108
return
106109
}
110+
111+
type lockingPRNG struct {
112+
*PRNG
113+
sync.Mutex
114+
}
115+
116+
func init() {
117+
p, err := NewPRNG()
118+
if err != nil {
119+
panic(err)
120+
}
121+
globalRand = &lockingPRNG{PRNG: p}
122+
}
123+
124+
func (p *lockingPRNG) Read(s []byte) (n int, err error) {
125+
p.Lock()
126+
defer p.Unlock()
127+
128+
return p.PRNG.Read(s)
129+
}

crypto/rand/prng_arc4random.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) 2024 The Decred developers
2+
// Use of this source code is governed by an ISC
3+
// license that can be found in the LICENSE file.
4+
5+
//go:build openbsd && go1.24
6+
7+
package rand
8+
9+
import (
10+
cryptorand "crypto/rand"
11+
)
12+
13+
// PRNG is a cryptographically secure pseudorandom number generator capable of
14+
// generating random bytes and integers. PRNG methods are not safe for
15+
// concurrent access.
16+
type PRNG struct{}
17+
18+
// NewPRNG returns a seeded PRNG.
19+
func NewPRNG() (*PRNG, error) {
20+
return new(PRNG), nil
21+
}
22+
23+
// Read fills s with len(s) of cryptographically-secure random bytes.
24+
// Read never errors.
25+
func (*PRNG) Read(s []byte) (n int, err error) {
26+
return cryptorand.Read(s)
27+
}
28+
29+
// stdlib crypto/rand can be read without extra locking.
30+
type lockingPRNG struct {
31+
PRNG
32+
}
33+
34+
func init() {
35+
globalRand = new(lockingPRNG)
36+
}
37+
38+
func (*lockingPRNG) Read(s []byte) (n int, err error) {
39+
return cryptorand.Read(s)
40+
}
41+
42+
func (*lockingPRNG) Lock() {}
43+
func (*lockingPRNG) Unlock() {}

0 commit comments

Comments
 (0)