关于golang:go优化记录01随机数值优化策略

38次阅读

共计 8206 个字符,预计需要花费 21 分钟才能阅读完成。

利用 Go 随机数值优化

用例代码:https://gitee.com/dn-jinmin/g…

01 根底性能实现优化

在 go 中对于随机 int 数值的办法能够利用规范库在的 ”math/rand” 进行实现;

package utils

import (
    "time"
    "math/rand"
)

func docGoRandInt32(maxN int) int {rand.Seed(time.Now().UnixNano())
    return rand.Intn(maxN)
}

测试用例

package utils

import "testing"

func Test_docGoRandInt32(t *testing.T) {
    for i := 0; i < 10; i++ {t.Logf("docGoRandInt32() = %v", docGoRandInt32(26))
    }
}

基准测试用例

func Benchmark_docGoRandInt32(b *testing.B) {
    for i := 0; i < b.N; i++ {docGoRandInt32(26)
    }
}

基于 bench 进行测试,如下为测试性能后果

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -bench=docGoRandInt32 -benchmem
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
Benchmark_docGoRandInt32-8        122416              8281 ns/op               0 B/op          0 allocs/op
PASS
ok      gitee.com/dn-jinmin/gen-id/utils        1.523s

依据测试结果显示对 docGoRandInt32 函数进行基准测试,每次调用 docGoRandInt32 函数耗费 8281ns,基于 122416 次调用的平均值,每次操作的上没有内存调配;

不过这个性能上的问题在利用中绝对较难承受,咱们能够利用 pprof 剖析起因, 执行如下命令

go test -bench=docGoRandInt32 -benchmem -cpuprofile=docGoRandInt32.prof
go tool pprof -http=:8000 docGoRandInt32.porf

能够发现性能瓶颈最终瓶颈在“rand/Seed”办法处

次要因为反复调用 ”math/rand.Seed” 函数导致;基于该景象调整如下

import (
    "time"
    "math/rand"
)

func init()  {rand.Seed(time.Now().UnixNano())
}

func docGoRandInt32(maxN int) int {return rand.Intn(maxN)
}

测试后果

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -v -run=doc -bench=doc -benchmem
=== RUN   Test_docGoRandInt32
    doc_test.go:7: docGoRandInt32() = 5
    doc_test.go:7: docGoRandInt32() = 1
    doc_test.go:7: docGoRandInt32() = 1
    doc_test.go:7: docGoRandInt32() = 3
    doc_test.go:7: docGoRandInt32() = 0
    doc_test.go:7: docGoRandInt32() = 14
    doc_test.go:7: docGoRandInt32() = 14
    doc_test.go:7: docGoRandInt32() = 20
    doc_test.go:7: docGoRandInt32() = 18
    doc_test.go:7: docGoRandInt32() = 8
--- PASS: Test_docGoRandInt32 (0.11s)
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
Benchmark_docGoRandInt32
Benchmark_docGoRandInt32-8      63955695                18.50 ns/op            0 B/op          0 allocs/op
PASS
ok      gitee.com/dn-jinmin/gen-id/utils        2.627s

02 基于并发测试调优

通过如上的形式根本实现了对于对随机数值的生成性能,并进行绝对的调优; 接下来基于并发下进行测试剖析如下为示例代码

func Test_go_docRandInt32(t *testing.T) {goHander(10, func()  {t.Logf("docGoRandInt32() = %v", docGoRandInt32(26))
    })
}

func Benchmark_go_docRandInt32(b *testing.B) {
    for i := 0; i < b.N; i++ {goHander(10, func()  {docGoRandInt32(26)
        })
    }
}

func goHander(workerNum int, handler func() )  {
    var sg sync.WaitGroup
    for i := 0; i < workerNum; i++ {sg.Add(1)
        go func() {defer sg.Done()
            handler()}()}
    sg.Wait()}

测试后果:

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -v -run=go_doc -bench=go_doc -benchmem -cpuprofile=go_docRandInt.prof
=== RUN   Test_go_docRandInt32
    doc_test.go:22: docGoRandInt32() = 12
    doc_test.go:22: docGoRandInt32() = 6
    doc_test.go:22: docGoRandInt32() = 18
    doc_test.go:22: docGoRandInt32() = 0
    doc_test.go:22: docGoRandInt32() = 5
    doc_test.go:22: docGoRandInt32() = 10
    doc_test.go:22: docGoRandInt32() = 3
    doc_test.go:22: docGoRandInt32() = 23
    doc_test.go:22: docGoRandInt32() = 18
    doc_test.go:22: docGoRandInt32() = 6
--- PASS: Test_go_docRandInt32 (0.00s)
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
Benchmark_go_docRandInt32
Benchmark_go_docRandInt32-8       244858              5333 ns/op              16 B/op          1 allocs/op
PASS
ok      gitee.com/dn-jinmin/gen-id/utils        1.714s

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>

性能实现上没问题,然而发现性能不佳;均匀执行实现需 5333ns;

go tool pprof -http=:8000 go_docRandInt.prof

剖析过程

基于 pprof 剖析在整体上并不能显著的发现问题所在;这个时候能够调整基于 trace 进行剖析查看协程的运行状态;批改代码如下

func Test_go_docRandInt32(t *testing.T) {setTrace("docGoRandInt32_trace.out", func(){goHander(10, func()  {docGoRandInt32(26)
        })
    })
}
func setTrace(name string, handler func()) {f, _ := os.Create(name)
    defer f.Close()
    // 启动 trace 对程序的执行进行剖析捕获
    err := trace.Start(f)
    if err != nil {panic(err)
    }
    defer trace.Stop()

    handler()}

执行命令

go test -v -run=go_doc
go tool trace docGoRandInt32_trace.out

运行后果

在整体代码的运行上发现并没有使用协程的特点,在执行流程中转向为同步执行

再对创立的协程运行状态察看

能够显著的发现存在较为重大的阻塞问题,再进一步的查看阻塞起因

能够显著发现是因为存在锁机制导致, 而后基于对 math/rand 源码浏览在底层实现 go/src/math/rand/rand.go:lockedSource.Int63() 中发现底层是对数据的读取中有使用加锁来保障线程平安

type lockedSource struct {
    lk  sync.Mutex
    src *rngSource
}

func (r *lockedSource) Int63() (n int64) {r.lk.Lock()
    n = r.src.Int63()
    r.lk.Unlock()
    return
}

对于该问题优化;间接上代码如下是来自一个开源作者的代码,我在开源根底上减少了对 uint64 的随机

var rngPool sync.Pool

type RNG struct {
    x uint32
    y uint64
}

func (r *RNG) Uint32() uint32 {
    for r.x == 0 {r.x = getRandomUint32()
    }

    // See https://en.wikipedia.org/wiki/Xorshift
    x := r.x
    x ^= x << 13
    x ^= x >> 17
    x ^= x << 5
    r.x = x
    return x
}

func (r *RNG) Uint32n(maxN uint32) uint32 {x := r.Uint32()
    // See http://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
    return uint32((uint64(x) * uint64(maxN)) >> 32)
}

func (r *RNG) Uint64() uint64 {
    for r.y == 0{r.y = getRandomUint64()
    }

    y := r.y
    y ^= y << 13
    y ^= y >> 7
    y ^= y << 5
    r.y = y
    return y
}
func (r *RNG) Uint64n(maxN uint64) uint64 {x := r.Uint64()
    return x % (maxN + 1)
}
func getRandomUint32() uint32 {x := time.Now().UnixNano()
    return uint32((x >> 32) ^ x)
}
func getRandomUint64() uint64 {x := time.Now().UnixNano()
    return uint64(x)
}

实现思路;

  1. 首先获取一个固定的随机因子通过 time.Now.UnixNano() 办法获取,会返回从 1970 到当初的工夫戳,通过 getRandomUint32 与 getRandomUint64 办法获取
  2. 构建属于本人的随机数值对象 rng;存在两个属性 x,y 别离记录随机因子 uint32 与 uint64
  3. 在 Uint32 与 Uint64 中通过对同一个随机因子进行“位运算 + 二元运算”重置每次调用的随机因子,确保具备随机性
  4. 须要留神在 Uint32 与 Uint64 在重置随机因子的时候,是通过复制以后参数给新的对象而后再通过运算重置,最初再设置原有数据结构,这样做的目标是防止并发状态下的数据呈现不确定性的问题

测试用例

func TestRNG_Int32(t *testing.T) {
    var rg RNG
    setTrace("docRngInt_trace.out", func(){goHander(10, func()  {rg.Uint32n(26)
        })
    })
}
func BenchmarkRNG_Int32(b *testing.B) {
    var rg RNG
    for i := 0; i < b.N; i++ {goHander(10, func()  {rg.Uint32n(26)
        })
    }
}

执行如下命令

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -v -run=RNG_Int32 -bench=RNG_Int32 -benchmem -cpuprofile=rng_int.prof
=== RUN   TestRNG_Int32
--- PASS: TestRNG_Int32 (0.00s)
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkRNG_Int32
BenchmarkRNG_Int32-8      327736              3345 ns/op              32 B/op          2 allocs/op
PASS
ok      gitee.com/dn-jinmin/gen-id/utils        1.488s

能够发现的确在性能上等到了肯定的晋升,通过对 trace 剖析在程序上就没有阻塞问题

03 进一步优化

如上的优化次要是针对于单次随机后果的性能优化,在理论的利用中会呈现的状况就是,可能具备不确定性间断随机的状况,比方我须要随机一个长度为 10 的数字要求每个数字都是随机组合的,或者须要随机一个长度为 20 的英文字母要求每个英文字母都是随机产生不能固定;

实现如下用例:

func TestRNG_Int32(t *testing.T) {
    var rg RNG
    setTrace("docRngInt_trace.out", func(){goHander(10, func()  {
            for i := 0; i < 10; i++ {rg.Uint32n(26)
            }
        })
    })
}
func BenchmarkRNG_Int32(b *testing.B) {
    var rg RNG
    for i := 0; i < b.N; i++ {goHander(10, func()  {
            for i := 0; i < 10; i++ {rg.Uint32n(26)
            }
        })
    }
}

如上代码,在整个测试用例中对 rg.Uint32n(26) 减少了一个 for 循环,循环次数 10 次;其含意在与能够代表以后的这个申请相当于每一次都要间断随机 10 个随机数;而随机数的范畴在 0~26 之间(代表随机 26 个英文字母)

测试后果:

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -v -run=RNG_Int32 -bench=RNG_Int32 -benchmem
=== RUN   TestRNG_Int32
--- PASS: TestRNG_Int32 (0.00s)
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkRNG_Int32
BenchmarkRNG_Int32-8      204184              6456 ns/op              32 B/op          2 allocs/op

针对优化,在 rng 根底上减少额定的代码封装

func RandIntHandler(maxN, i int, hander func(num, i int))  {var y uint32 = rng.Uint32()

    for  {
        if i == 0{return}
        i--
        // 运算办法
        hander(int((uint64(y) * uint64(maxN)) >> 32), i)

        y ^= y << 13
        y ^= y >> 17
        y ^= y << 5
    }
}

在代码中通过 rng.Uint32()获取一个随机因子存在 y 中,而后通过 y 先进行计算随机的数值并传入到 hander 办法中,再应用完之后通过同样的“位运算 + 二元运算”对随机因子进行“重置”,同时再进行 len– 代表须要随机的次数减一

测试用例:

func TestRNG_Int32Hander(t *testing.T) {setTrace("docRngInt_trace.out", func(){goHander(10, func()  {RandIntHandler(26,10, func(a,i int) {})
        })
    })
}
func BenchmarkRNG_Int32Hander(b *testing.B) {
    for i := 0; i < b.N; i++ {goHander(10, func()  {RandIntHandler(26,10, func(a,i int) {})
        })
    }
}

测试后果与 RNG_Int32 比照

D:\project\go\src\gitee.com\dn-jinmin\gen-id\utils>go test -bench=RNG_Int32 -benchmem
goos: windows
goarch: amd64
pkg: gitee.com/dn-jinmin/gen-id/utils
cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz
BenchmarkRNG_Int32-8              183133              6135 ns/op              32 B/op          2 allocs/op
BenchmarkRNG_Int32Hander-8        203179              5323 ns/op              16 B/op          1 allocs/op
PASS
ok      gitee.com/dn-jinmin/gen-id/utils        2.480s

绝对具备肯定的晋升,次要在于在 RandIntHander 中缩小了对间断随机的因子的拷贝次数,在 rng.Uint32 中因为须要思考并发的状况,会先拷贝重置,而后再赋值的过程

x := r.x
x ^= x << 13
x ^= x >> 17
x ^= x << 5
r.x = x

而在 RandIntHander 中获取到随机数值并赋值给 y 之后便始终围绕 y 进行解决

var y uint32 = rng.Uint32()

for {
    ...

    y ^= y << 13
    y ^= y >> 17
    y ^= y << 5
}

目前优化到此处,后续再尝试看是否再进一步优化

04 总结

  1. math/rand 的实现

在 go 提供的规范包中的 math/rand 在实现上是须要咱们传递一个随机因子,而在调用随机函数的时候在 go/src/math/rand/rand.go:lockedSource.Int63() 中通过利用到锁机制来实现线程平安问题

type lockedSource struct {
    lk  sync.Mutex
    src *rngSource
}

func (r *lockedSource) Int63() (n int64) {r.lk.Lock()
    n = r.src.Int63()
    r.lk.Unlock()
    return
}

在理论的援用中会存在协程阻塞的问题;而对其根本的优化就是能够将设置随机种子 rand.Seed(time.Now().UnixNano()) 的过程放在 init 中初始化;

  1. 本人实现

实现的思路上就是获取一个随机数值而后始终围绕这个随机数值进行随机使用算,先获取所须要的理论随机值,而后再对随机数值通过固定的计划运算将其随机数值改为其余值以此达到随机的目标;

在本次案例中先是获取 time.Now().UnixNano() 以后工夫作为随机数值,也能够统称为随机因子;

再失去因子之后通过(uint64(y) * uint64(maxN)) >> 32 位运算的策略,也能够取模的形式获取随机值

通过如下代码重置随机因子

x := r.x
x ^= x << 13
x ^= x >> 17
x ^= x << 5
r.x = x

在细节上 x := r.x 这一步的赋值是因为防止并发下间接对 r.x 操作可能会存在数值不确定性的问题;再进一步细节一点的优化能够围绕这个思路再封装一个 RandIntHander 办法缩小间断随机中的赋值参数的拷贝次数

.

正文完
 0