Go 的 once 类型的效率测量

我有一段代码,我只想运行一次以进行初始化。到目前为止,我使用 sync.Mutex 结合 if 子句来测试它是否已经运行。后来我在同一个同步包中遇到了 Once 类型及其 DO() 函数。

实现如下https://golang.org/src/sync/once.go

func (o *Once) Do(f func()) {

    if atomic.LoadUint32(&o.done) == 1 {

        return

    }

    // Slow-path.

    o.m.Lock()

    defer o.m.Unlock()

    if o.done == 0 {

        defer atomic.StoreUint32(&o.done, 1)

        f()

    }

}

查看代码,它基本上是我之前一直在使用的相同的东西。与 if 子句结合的互斥量。但是,添加的函数调用使我觉得这看起来效率很低。我做了一些测试并尝试了各种版本:


func test1() {

    o.Do(func() {

        // Do smth

    })

    wg.Done()

}


func test2() {

    m.Lock()

    if !b {

        func() {

            // Do smth

        }()

    }

    b = true

    m.Unlock()

    wg.Done()

}


func test3() {

    if !b {

        m.Lock()

        if !b {

            func() {

                // Do smth

            }()

            b = true

        }

        m.Unlock()

    }

    wg.Done()

}

我通过运行以下代码测试了所有版本:


    wg.Add(10000)

    start = time.Now()

    for i := 0; i < 10000; i++ {

        go testX()

    }

    wg.Wait()

    end = time.Now()


    fmt.Printf("elapsed: %v\n", end.Sub(start).Nanoseconds())

结果如下:


elapsed: 8002700 //test1

elapsed: 5961600 //test2

elapsed: 5646700 //test3

甚至值得使用 Once 类型吗?它很方便,但性能甚至比始终序列化所有例程的 test2 更差。


另外,为什么他们在 if 子句中使用 atomic int?无论如何,存储都发生在锁内。


编辑:Go playground 链接:https://play.golang.org/p/qlMxPYop7kS注意:这不会显示结果,因为 playground 的时间是固定的。


潇潇雨雨
浏览 129回答 1
1回答

繁星点点滴滴

那不是您应该测试代码性能的方式。您应该使用 Go 的内置测试框架(testing包和go test命令)。让我们创建可测试代码:func f() {    // Code that must only be run once}var testOnce = &sync.Once{}func DoWithOnce() {    testOnce.Do(f)}var (    mu = &sync.Mutex{}    b  bool)func DoWithMutex() {    mu.Lock()    if !b {        f()        b = true    }    mu.Unlock()}让我们使用该testing包编写适当的测试/基准测试代码:func BenchmarkOnce(b *testing.B) {    for i := 0; i < b.N; i++ {        DoWithOnce()    }}func BenchmarkMutex(b *testing.B) {    for i := 0; i < b.N; i++ {        DoWithMutex()    }}我们可以使用以下代码运行基准测试:go test -bench .以下是基准测试结果:BenchmarkOnce-4         200000000                6.30 ns/opBenchmarkMutex-4        100000000               20.0 ns/opPASS如您所见,使用sync.Once()比使用sync.Mutex. 为什么?因为sync.Once()有一个“优化”的短路径,它只使用原子加载来检查任务之前是否被调用过,如果是,则不使用互斥锁。“慢速”路径可能只在第一次调用Once.Do(). 虽然如果你有许多并发的 goroutines 试图调用DoWithOnce(),慢速路径可能会多次到达,但从长远来看once.Do()只需要使用原子负载。并行测试(来自多个 goroutines)是的,上面的基准测试代码仅使用单个 goroutine 进行测试。但是使用多个并发 goroutine 只会让互斥体的情况变得更糟,因为它总是必须获得一个互斥体来检查是否要调用任务,而只sync.Once使用原子负载。尽管如此,让我们对其进行基准测试。以下是使用并行测试的基准测试代码:func BenchmarkOnceParallel(b *testing.B) {    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            DoWithOnce()        }    })}func BenchmarkMutexParallel(b *testing.B) {    b.RunParallel(func(pb *testing.PB) {        for pb.Next() {            DoWithMutex()        }    })}我的机器上有 4 个内核,所以我将使用这 4 个内核:go test -bench Parallel -cpu=4(您可以省略该-cpu标志,在这种情况下,它默认为GOMAXPROCS– 可用核心数。)结果如下:BenchmarkOnceParallel-4         500000000                3.04 ns/opBenchmarkMutexParallel-4        20000000                93.7 ns/op当“并发增加”时,结果开始变得无与伦比sync.Once(在上面的测试中,它快了 30 倍)。我们可能会进一步增加使用创建的 goroutines 的数量testing.B.SetPralleism(),但是当我将它设置为 100 时我得到了类似的结果(这意味着 400 个 goroutines 被用来调用基准测试代码)。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Go