神一次效率测量型
Efficiency measurments of Go's once Type
我有一段代码,我只想 运行 初始化一次。
到目前为止,我使用 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?无论如何,存储都发生在锁内。
编辑:去游乐场link:https://play.golang.org/p/qlMxPYop7kS注意:由于游乐场的时间固定,因此不会显示结果。
这不是您测试代码性能的方式。您应该使用 Go 的内置测试框架(testing
package and go test
command). See 了解详情。
让我们创建可测试代码:
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
快将近 4 倍。为什么?因为 sync.Once()
有一个 "optimized",仅使用原子加载来检查之前是否调用过任务的短路径,如果有,则不使用互斥锁。 "slow" 路径可能仅在第一次调用 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
当 "concurrency increases" 时,结果开始变得无与伦比 sync.Once
(在上面的测试中,它快了 30 倍)。
我们可能会进一步增加使用 testing.B.SetPralleism()
创建的 goroutines 的数量,但是当我将它设置为 100 时我得到了类似的结果(这意味着 400 个 goroutines 被用于调用基准测试代码)。
我有一段代码,我只想 运行 初始化一次。 到目前为止,我使用 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?无论如何,存储都发生在锁内。
编辑:去游乐场link:https://play.golang.org/p/qlMxPYop7kS注意:由于游乐场的时间固定,因此不会显示结果。
这不是您测试代码性能的方式。您应该使用 Go 的内置测试框架(testing
package and go test
command). See
让我们创建可测试代码:
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
快将近 4 倍。为什么?因为 sync.Once()
有一个 "optimized",仅使用原子加载来检查之前是否调用过任务的短路径,如果有,则不使用互斥锁。 "slow" 路径可能仅在第一次调用 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
当 "concurrency increases" 时,结果开始变得无与伦比 sync.Once
(在上面的测试中,它快了 30 倍)。
我们可能会进一步增加使用 testing.B.SetPralleism()
创建的 goroutines 的数量,但是当我将它设置为 100 时我得到了类似的结果(这意味着 400 个 goroutines 被用于调用基准测试代码)。