为了账号安全,请及时绑定邮箱和手机立即绑定

Go 的 once 类型的效率测量

Go 的 once 类型的效率测量

Go
潇潇雨雨 2023-05-08 17:54:55
我有一段代码,我只想运行一次以进行初始化。到目前为止,我使用 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 //test1elapsed: 5961600 //test2elapsed: 5646700 //test3甚至值得使用 Once 类型吗?它很方便,但性能甚至比始终序列化所有例程的 test2 更差。另外,为什么他们在 if 子句中使用 atomic int?无论如何,存储都发生在锁内。编辑:Go playground 链接:https://play.golang.org/p/qlMxPYop7kS注意:这不会显示结果,因为 playground 的时间是固定的。
查看完整描述

1 回答

?
繁星点点滴滴

TA贡献1803条经验 获得超3个赞

那不是您应该测试代码性能的方式。您应该使用 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/op

BenchmarkMutex-4        100000000               20.0 ns/op

PASS

如您所见,使用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/op

BenchmarkMutexParallel-4        20000000                93.7 ns/op

当“并发增加”时,结果开始变得无与伦比sync.Once(在上面的测试中,它快了 30 倍)。


我们可能会进一步增加使用创建的 goroutines 的数量testing.B.SetPralleism(),但是当我将它设置为 100 时我得到了类似的结果(这意味着 400 个 goroutines 被用来调用基准测试代码)。


查看完整回答
反对 回复 2023-05-08
  • 1 回答
  • 0 关注
  • 128 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信