Skip to content

Commit

Permalink
rand: Use stdlib crypto/rand.Read on OpenBSD + Go 1.24
Browse files Browse the repository at this point in the history
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%
  • Loading branch information
jrick committed Dec 17, 2024
1 parent 752dac0 commit 692cb83
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 49 deletions.
3 changes: 3 additions & 0 deletions crypto/rand/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ The default global PRNG will never panic after package init and is safe for
concurrent access. Additional PRNGs which avoid the locking overhead can be
created by calling `NewPRNG`.

On select operating systems and Go versions, this package may fallback to
`crypto/rand` when it is already implemented by a fast userspace CSPRNG.

## Statistical Test Quality Assessment Results

The quality of the random number generation provided by this implementation has
Expand Down
77 changes: 28 additions & 49 deletions crypto/rand/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package rand
import (
"io"
"math/big"
"sync"
"time"
)

Expand All @@ -18,28 +17,8 @@ func Reader() io.Reader {
return globalRand
}

type lockingPRNG struct {
*PRNG
mu sync.Mutex
}

var globalRand *lockingPRNG

func init() {
p, err := NewPRNG()
if err != nil {
panic(err)
}
globalRand = &lockingPRNG{PRNG: p}
}

func (p *lockingPRNG) Read(s []byte) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()

return p.PRNG.Read(s)
}

// Read fills b with random bytes obtained from the default userspace PRNG.
func Read(b []byte) {
// Mutex is acquired by (*lockingPRNG).Read.
Expand All @@ -48,41 +27,41 @@ func Read(b []byte) {

// Uint32 returns a uniform random uint32.
func Uint32() uint32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Uint32()
}

// Uint64 returns a uniform random uint64.
func Uint64() uint64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Uint64()
}

// Uint32N returns a random uint32 in range [0,n) without modulo bias.
func Uint32N(n uint32) uint32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Uint32N(n)
}

// Uint64N returns a random uint32 in range [0,n) without modulo bias.
func Uint64N(n uint64) uint64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Uint64N(n)
}

// Int32 returns a random 31-bit non-negative integer as an int32 without
// modulo bias.
func Int32() int32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Int32()
}
Expand All @@ -91,17 +70,17 @@ func Int32() int32 {
// without modulo bias.
// Panics if n <= 0.
func Int32N(n int32) int32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Int32N(n)
}

// Int64 returns a random 63-bit non-negative integer as an int64 without
// modulo bias.
func Int64() int64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Int64()
}
Expand All @@ -110,16 +89,16 @@ func Int64() int64 {
// without modulo bias.
// Panics if n <= 0.
func Int64N(n int64) int64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Int64N(n)
}

// Int returns a non-negative integer without bias.
func Int() int {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Int()
}
Expand All @@ -128,25 +107,25 @@ func Int() int {
// modulo bias.
// Panics if n <= 0.
func IntN(n int) int {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.IntN(n)
}

// UintN returns, as an uint, a random integer in [0,n) without modulo bias.
func UintN(n uint) uint {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.UintN(n)
}

// Duration returns a random duration in [0,n) without modulo bias.
// Panics if n <= 0.
func Duration(n time.Duration) time.Duration {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.Duration(n)
}
Expand All @@ -155,8 +134,8 @@ func Duration(n time.Duration) time.Duration {
// indexes i and j.
// Panics if n < 0.
func Shuffle(n int, swap func(i, j int)) {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

globalRand.Shuffle(n, swap)
}
Expand All @@ -171,8 +150,8 @@ func ShuffleSlice[S ~[]E, E any](s S) {
// Int returns a uniform random value in [0,max).
// Panics if max <= 0.
func BigInt(max *big.Int) *big.Int {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()
globalRand.Lock()
defer globalRand.Unlock()

return globalRand.PRNG.BigInt(max)
}
Expand Down
3 changes: 3 additions & 0 deletions crypto/rand/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
// The default global PRNG will never panic after package init and is safe for
// concurrent access. Additional PRNGs which avoid the locking overhead can
// be created by calling NewPRNG.
//
// On select operating systems and Go versions, this package may fallback to
// crypto/rand when it is already implemented by a fast userspace CSPRNG.
package rand
23 changes: 23 additions & 0 deletions crypto/rand/prng.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

//go:build !(openbsd && go1.24)

package rand

import (
cryptorand "crypto/rand"
"encoding/binary"
"math/bits"
"sync"
"time"

"golang.org/x/crypto/chacha20"
Expand Down Expand Up @@ -104,3 +107,23 @@ func (p *PRNG) Read(s []byte) (n int, err error) {
n += len(s)
return
}

type lockingPRNG struct {
*PRNG
sync.Mutex
}

func init() {
p, err := NewPRNG()
if err != nil {
panic(err)
}
globalRand = &lockingPRNG{PRNG: p}
}

func (p *lockingPRNG) Read(s []byte) (n int, err error) {
p.Lock()
defer p.Unlock()

return p.PRNG.Read(s)
}
43 changes: 43 additions & 0 deletions crypto/rand/prng_arc4random.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

//go:build openbsd && go1.24

package rand

import (
cryptorand "crypto/rand"
)

// PRNG is a cryptographically secure pseudorandom number generator capable of
// generating random bytes and integers. PRNG methods are not safe for
// concurrent access.
type PRNG struct{}

// NewPRNG returns a seeded PRNG.
func NewPRNG() (*PRNG, error) {
return new(PRNG), nil
}

// Read fills s with len(s) of cryptographically-secure random bytes.
// Read never errors.
func (*PRNG) Read(s []byte) (n int, err error) {
return cryptorand.Read(s)
}

// stdlib crypto/rand can be read without extra locking.
type lockingPRNG struct {
PRNG
}

func init() {
globalRand = new(lockingPRNG)
}

func (*lockingPRNG) Read(s []byte) (n int, err error) {
return cryptorand.Read(s)
}

func (*lockingPRNG) Lock() {}
func (*lockingPRNG) Unlock() {}

0 comments on commit 692cb83

Please sign in to comment.