首页 > 解决方案 > 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 子句中使用原子 int?无论如何,存储都发生在锁内。

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

标签: performancegosynchronizationmutex

解决方案


这不是你应该测试代码性能的方式。您应该使用 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(),只需要使用原子负载。

并行测试(来自多个 goroutine)

是的,上面的基准测试代码只使用了一个 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 倍)。

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


推荐阅读