Это не то, как вы должны тестировать производительность кода.Вы должны использовать встроенную среду тестирования 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()
было почти в 4 раза быстрее, чем использование sync.Mutex
.Зачем?Поскольку sync.Once()
имеет «оптимизированный» короткий путь, который использует только атомарную загрузку, чтобы проверить, была ли задача вызвана ранее, и если это так, мьютекс не используется.«Медленный» путь, вероятно, используется только один раз при первом вызове Once.Do()
.Хотя, если у вас будет много одновременных попыток вызова DoWithOnce()
, медленный путь может быть достигнут несколько раз, но в долгосрочной перспективе once.Do()
потребуется использовать только атомную загрузку.
Параллельное тестирование(из нескольких программ)
Да, в приведенном выше коде сравнительного анализа для тестирования используется только одна программа.Но использование нескольких одновременных программ только ухудшит ситуацию с мьютексом, поскольку ему всегда нужно получить мьютекс, чтобы даже проверить, должна ли быть вызвана задача, в то время как 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 раз быстрее).
Мы можем еще больше увеличить числоколичество процедур, созданных с использованием testing.B.SetPralleism()
, но я получил аналогичный результат, когда установил его на 100 (это означает, что 400 вызовов были использованы для вызова кода тестирования).