Почему блокировка в Go намного медленнее, чем в Java? Много времени, проведенного в Mutex.Lock() Mutex.Unlock()
Я написал небольшую библиотеку Go ( go-patan), которая собирает запущенные min / max / avg / stddev определенных переменных. Я сравнил его с эквивалентной реализацией Java ( patan), и, к моему удивлению, реализация Java стала намного быстрее. Я хотел бы понять почему.
Библиотека в основном состоит из простого хранилища данных с блокировкой, которая сериализует чтение и запись. Это фрагмент кода:
type Store struct {
durations map[string]*Distribution
counters map[string]int64
samples map[string]*Distribution
lock *sync.Mutex
}
func (store *Store) addSample(key string, value int64) {
store.addToStore(store.samples, key, value)
}
func (store *Store) addDuration(key string, value int64) {
store.addToStore(store.durations, key, value)
}
func (store *Store) addToCounter(key string, value int64) {
store.lock.Lock()
defer store.lock.Unlock()
store.counters[key] = store.counters[key] + value
}
func (store *Store) addToStore(destination map[string]*Distribution, key string, value int64) {
store.lock.Lock()
defer store.lock.Unlock()
distribution, exists := destination[key]
if !exists {
distribution = NewDistribution()
destination[key] = distribution
}
distribution.addSample(value)
}
Я провел сравнительный анализ реализации GO и Java ( go-benchmark-gist, java-benchmark-gist) и побед Java, но я не понимаю, почему:
Go Results:
10 threads with 20000 items took 133 millis
100 threads with 20000 items took 1809 millis
1000 threads with 20000 items took 17576 millis
10 threads with 200000 items took 1228 millis
100 threads with 200000 items took 17900 millis
Java Results:
10 threads with 20000 items takes 89 millis
100 threads with 20000 items takes 265 millis
1000 threads with 20000 items takes 2888 millis
10 threads with 200000 items takes 311 millis
100 threads with 200000 items takes 3067 millis
Я профилировал программу с помощью pprof Go и сгенерировал граф вызовов. Это показывает, что он в основном все время проводит в синхронизации.(*Mutex).Lock() и sync. (* Mutex).Unlock ().
Top20 звонит по данным профилировщика:
(pprof) top20
59110ms of 73890ms total (80.00%)
Dropped 22 nodes (cum <= 369.45ms)
Showing top 20 nodes out of 65 (cum >= 50220ms)
flat flat% sum% cum cum%
8900ms 12.04% 12.04% 8900ms 12.04% runtime.futex
7270ms 9.84% 21.88% 7270ms 9.84% runtime/internal/atomic.Xchg
7020ms 9.50% 31.38% 7020ms 9.50% runtime.procyield
4560ms 6.17% 37.56% 4560ms 6.17% sync/atomic.CompareAndSwapUint32
4400ms 5.95% 43.51% 4400ms 5.95% runtime/internal/atomic.Xadd
4210ms 5.70% 49.21% 22040ms 29.83% runtime.lock
3650ms 4.94% 54.15% 3650ms 4.94% runtime/internal/atomic.Cas
3260ms 4.41% 58.56% 3260ms 4.41% runtime/internal/atomic.Load
2220ms 3.00% 61.56% 22810ms 30.87% sync.(*Mutex).Lock
1870ms 2.53% 64.10% 1870ms 2.53% runtime.osyield
1540ms 2.08% 66.18% 16740ms 22.66% runtime.findrunnable
1430ms 1.94% 68.11% 1430ms 1.94% runtime.freedefer
1400ms 1.89% 70.01% 1400ms 1.89% sync/atomic.AddUint32
1250ms 1.69% 71.70% 1250ms 1.69% github.com/toefel18/go-patan/statistics/lockbased.(*Distribution).addSample
1240ms 1.68% 73.38% 3140ms 4.25% runtime.deferreturn
1070ms 1.45% 74.83% 6520ms 8.82% runtime.systemstack
1010ms 1.37% 76.19% 1010ms 1.37% runtime.newdefer
1000ms 1.35% 77.55% 1000ms 1.35% runtime.mapaccess1_faststr
950ms 1.29% 78.83% 15660ms 21.19% runtime.semacquire
860ms 1.16% 80.00% 50220ms 67.97% main.Benchmrk.func1
Может кто-нибудь объяснить, почему блокировка в Go кажется намного медленнее, чем в Java, что я делаю не так? Я также написал канальную реализацию на Go, но это еще медленнее.
2 ответа
Я также разместил этот вопрос в группе golang-nuts. В ответе Джеспера Луиса Андерсена достаточно хорошо объясняется, что в Java используются методы оптимизации синхронизации, такие как анализ выхода из блокировки / удаление блокировки и укрупнение блокировки.
Java JIT может брать блокировку и разрешать несколько обновлений одновременно в блокировке для повышения производительности. Я запустил тест Java с -Djava.compiler=NONE
который дал драматическое представление, но это не честное сравнение.
Я предполагаю, что многие из этих методов оптимизации оказывают меньшее влияние в производственной среде.
Лучше всего избегать defer
в крошечных функциях, которые требуют высокой производительности, так как это дорого. В большинстве других случаев нет необходимости избегать этого, так как стоимость defer
перевешивается кодом вокруг него.
Я также рекомендую использовать lock sync.Mutex
вместо использования указателя. Указатель создает небольшое количество дополнительной работы для программиста (инициализация, nil
ошибки), и небольшое количество дополнительной работы для сборщика мусора.