Performance Go的一次型效率测度

Performance Go的一次型效率测度,performance,go,synchronization,mutex,Performance,Go,Synchronization,Mutex,我有一段代码,我只想为初始化运行一次。 到目前为止,我使用sync.Mutex和if子句来测试它是否已经运行过。后来我在同一个sync包中遇到了Once类型及其DO函数 实施情况如下: 看看代码,它基本上和我以前用过的一样。与if子句组合的互斥体。然而,添加的函数调用使我觉得这相当低效。我做了一些测试并尝试了各种版本: func test1() { o.Do(func() { // Do smth }) wg.Done() } func test2()

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

实施情况如下:

看看代码,它基本上和我以前用过的一样。与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())
go test -bench .
是否值得使用Once类型?它很方便,但性能甚至比test2差,test2总是序列化所有例程

还有,为什么他们在if子句中使用原子int?无论如何,存储都发生在锁内


编辑:转到操场链接:注意:这不会显示结果,因为操场上的时间是固定的。

这不是测试代码性能的方法。您应该使用Go的内置测试框架包和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()
}
让我们使用测试包编写适当的测试/基准测试代码:

以下是基准测试结果:

BenchmarkOnce-4         200000000                6.30 ns/op
BenchmarkMutex-4        100000000               20.0 ns/op
PASS
如您所见,使用sync.one几乎比使用sync.Mutex快4倍。为什么?因为sync.Once有一个优化的短路径,它只使用一个原子负载来检查任务之前是否被调用过,如果是,则不使用互斥。慢路径可能只在第一次调用once.Do时使用一次。虽然如果您有许多并发goroutine试图调用dowithone,那么慢路径可能会多次到达,但从长远来看只有一次。Do只需要使用原子负载

来自多个goroutine的并行测试 是的,上面的基准测试代码只使用一个goroutine进行测试。但是使用多个并发goroutine只会使互斥体的情况变得更糟,因为它总是需要获得互斥体,以便在同步时检查任务是否被调用

尽管如此,让我们对其进行基准测试

以下是使用并行测试的基准测试代码:

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
当并发性增加时,结果开始变得不可压缩,有利于同步。一旦在上面的测试中,它会快30倍


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

您的Go测试包基准测试结果在哪里?test3存在数据竞争,如果不同步,您无法读取b。如果您将该检查移动到受互斥锁保护的块内,您已经做得比以前更糟了。使用原子负载优化了短路径。慢路径最有可能只遇到一次。请参阅test2,它在锁定部分中对b进行检查,速度要快得多。正如peterSO所写,我们不知道您是如何获得测试结果的。显示测试和基准测试代码。首先,使用Go内置的基准测试系统,它是非常彻底和有效的。其次,除非你已经对一个真实世界的应用程序进行了基准测试,发现了一个性能问题,并使用评测跟踪该问题以进行同步。在任何现实场景中,sync.Once都不太可能对性能产生任何有意义的影响。谢谢,这完全解决了我的困惑。另外,我不知道测试包的基准测试功能。从现在起,我将使用它。但是,我怎么知道它会同时运行多个goroutine呢?我不确定是不是。应该是:go DoWithOnce和go DoWithMutex吗?@Gilrich您不应该在基准测试代码中使用go,这将使结果几乎毫无用处。但在那里,你会得到类似的结果。检查编辑的答案。
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()
        }
    })
}
go test -bench Parallel -cpu=4
BenchmarkOnceParallel-4         500000000                3.04 ns/op
BenchmarkMutexParallel-4        20000000                93.7 ns/op