Данный способ — это всего лишь спортивный эксперимент, накиданный под вечер на коленке с целью что-нибудь исследовать.
Задача была написать аналог sync.Map максимально приближенный по производительности к оригиналу, спортивного интереса ради.
Давайте сразу к коду и бенчмаркам:
package main import ( "hash/fnv" "sync" ) type bucket struct { data sync.Map } type FastSyncMap struct { buckets []*bucket } func NewSyncMap(bucketCount int) *FastSyncMap { buckets := make([]*bucket, bucketCount) for i := range buckets { buckets[i] = &bucket{} } return &FastSyncMap{buckets: buckets} } func (m *FastSyncMap) hash(key string) int { h := fnv.New32a() h.Write([]byte(key)) return int(h.Sum32()) % len(m.buckets) } func (m *FastSyncMap) Store(key string, value interface{}) { index := m.hash(key) b := m.buckets[index] b.data.Store(key, value) } func (m *FastSyncMap) Load(key string) (value interface{}, ok bool) { index := m.hash(key) b := m.buckets[index] return b.data.Load(key) }
Вот так выглядит бенчмарк:
package main import ( "strconv" "sync" "testing" ) var data = map[string]int{ "key-001": 437, "key-002": 384, "key-003": 983, "key-004": 543, "key-005": 276, "key-006": 128, "key-007": 753, "key-008": 832, "key-009": 621, "key-010": 519, "key-011": 407, "key-012": 905, "key-013": 132, "key-014": 658, "key-015": 294, "key-016": 745, "key-017": 389, "key-018": 910, "key-019": 482, "key-020": 371, "key-021": 648, "key-022": 573, "key-023": 781, "key-024": 452, "key-025": 390, "key-026": 167, "key-027": 296, "key-028": 854, "key-029": 731, "key-030": 513, "key-031": 249, "key-032": 675, "key-033": 890, "key-034": 412, "key-035": 567, "key-036": 712, "key-037": 361, "key-038": 284, "key-039": 946, "key-040": 709, "key-041": 541, "key-042": 673, "key-043": 325, "key-044": 498, "key-045": 823, "key-046": 654, "key-047": 832, "key-048": 276, "key-049": 792, "key-050": 619, "key-051": 485, "key-052": 527, "key-053": 831, "key-054": 491, "key-055": 350, "key-056": 764, "key-057": 539, "key-058": 204, "key-059": 926, "key-060": 687, "key-061": 498, "key-062": 143, "key-063": 359, "key-064": 871, "key-065": 604, "key-066": 318, "key-067": 425, "key-068": 691, "key-069": 536, "key-070": 174, "key-071": 523, "key-072": 680, "key-073": 352, "key-074": 870, "key-075": 429, "key-076": 237, "key-077": 491, "key-078": 391, "key-079": 702, "key-080": 826, "key-081": 318, "key-082": 563, "key-083": 496, "key-084": 791, "key-085": 142, "key-086": 380, "key-087": 943, "key-088": 467, "key-089": 389, "key-090": 905, "key-091": 281, "key-092": 478, "key-093": 654, "key-094": 193, "key-095": 849, "key-096": 740, "key-097": 268, "key-098": 431, "key-099": 739, "key-100": 511, "key-101": 234, "key-102": 593, "key-103": 800, "key-104": 137, "key-105": 405, "key-106": 872, "key-107": 364, "key-108": 230, "key-109": 793, "key-110": 649, "key-111": 507, "key-112": 391, "key-113": 170, "key-114": 923, "key-115": 478, "key-116": 375, "key-117": 682, "key-118": 195, "key-119": 735, "key-120": 341, "key-121": 263, "key-122": 498, "key-123": 149, "key-124": 517, "key-125": 350, "key-126": 630, "key-127": 809, "key-128": 472, "key-129": 367, "key-130": 953, "key-131": 284, "key-132": 576, "key-133": 395, "key-134": 719, "key-135": 364, "key-136": 157, "key-137": 861, "key-138": 495, "key-139": 267, "key-140": 436, "key-141": 513, "key-142": 722, "key-143": 184, "key-144": 637, "key-145": 912, "key-146": 501, "key-147": 294, "key-148": 178, "key-149": 843, "key-150": 671, } func BenchmarkMySyncMap(b *testing.B) { m := NewSyncMap(len(data)) b.ResetTimer() for i := 0; i < b.N; i++ { for k, v := range data { m.Store(k, v) } for k, v := range data { v2, ok := m.Load(k) if !ok || v2 != v { b.Fatalf("key %s not found in cache (%v) or incorrect value: %v", k, ok, v2) } } } } func BenchmarkGoSyncMap(b *testing.B) { var m sync.Map b.ResetTimer() for i := 0; i < b.N; i++ { for k, v := range data { m.Store(k, v) } for k, v := range data { v2, ok := m.Load(k) if !ok || v2 != v { b.Fatalf("key %s not found in cache (%v) or incorrect value: %v", k, ok, v2) } } } } func BenchmarkMySyncMapParallel(b *testing.B) { m := NewSyncMap(150 * 2) b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { for k, v := range data { m.Store(k, v) } for k, v := range data { v2, ok := m.Load(k) if !ok || v2 != v { b.Fatalf("key %s not found in cache (%v) or incorrect value: %v", k, ok, v2) } } } }) } func BenchmarkGoSyncMapParallel(b *testing.B) { var m sync.Map b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { for k, v := range data { m.Store(k, v) } for k, v := range data { v2, ok := m.Load(k) if !ok || v2 != v { b.Fatalf("key %s not found in cache (%v) or incorrect value: %v", k, ok, v2) } } } }) } func BenchmarkFastSyncMapWriteMillion(b *testing.B) { m := NewSyncMap(150 * 2) totalEntries := 1000000 b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { for i := 0; i < totalEntries; i++ { key := strconv.Itoa(i) m.Store(key, i) m.Load(key) } } }) } func BenchmarkSyncMapWriteMillion(b *testing.B) { var m sync.Map totalEntries := 1000000 b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { for i := 0; i < totalEntries; i++ { key := strconv.Itoa(i) m.Store(key, i) m.Load(key) } } }) }
Результат выполнения бенчмарка на моем ПК
goos: linux goarch: amd64 pkg: lesson cpu: 12th Gen Intel(R) Core(TM) i7-12700K BenchmarkMySyncMap BenchmarkMySyncMap-20 74986 15030 ns/op BenchmarkGoSyncMap BenchmarkGoSyncMap-20 76708 15605 ns/op BenchmarkMySyncMapParallel BenchmarkMySyncMapParallel-20 479565 2518 ns/op BenchmarkGoSyncMapParallel BenchmarkGoSyncMapParallel-20 455433 2679 ns/op BenchmarkFastSyncMapWriteMillion BenchmarkFastSyncMapWriteMillion-20 4 264691361 ns/op BenchmarkSyncMapWriteMillion BenchmarkSyncMapWriteMillion-20 3 458739675 ns/op PASS
Здесь мы видим, что записи\чтении в одном потоке стандартная sync.Map выигрывает.
При конкурентном доступе с малой нагрузкой fast sync map быстрее на 2%.
При конкурентном доступе с нагрузкой в миллион записей fast sync map быстрее на 73%.
Данные моей тачки, где выполнялся бенчмарк:
$ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 46 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 12th Gen Intel(R) Core(TM) i7-12700K CPU family: 6 Model: 151 Thread(s) per core: 2 Core(s) per socket: 12 Socket(s): 1 Stepping: 2 CPU max MHz: 5000,0000 CPU min MHz: 800,0000 BogoMIPS: 7219.20 Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bt s rep_good nopl xtopology nonstop_tsc cpuid aperfmperf tsc_known_freq pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault ssbd ibrs ibpb stibp ibrs_enhanced tpr_shadow flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 avx 2 smep bmi2 erms invpcid rdseed adx smap clflushopt clwb intel_pt sha_ni xsaveopt xsavec xgetbv1 xsaves split_lock_detect avx_vnni dtherm ida arat pln pts hwp hwp_notify hwp_act_window h wp_epp hwp_pkg_req hfi vnmi umip pku ospke waitpkg gfni vaes vpclmulqdq tme rdpid movdiri movdir64b fsrm md_clear serialize pconfig arch_lbr ibt flush_l1d arch_capabilities Virtualization features: Virtualization: VT-x Caches (sum of all): L1d: 512 KiB (12 instances) L1i: 512 KiB (12 instances) L2: 12 MiB (9 instances) L3: 25 MiB (1 instance) NUMA: NUMA node(s): 1 NUMA node0 CPU(s): 0-19 Vulnerabilities: Gather data sampling: Not affected Itlb multihit: Not affected L1tf: Not affected Mds: Not affected Meltdown: Not affected Mmio stale data: Not affected Retbleed: Not affected Spec rstack overflow: Not affected Spec store bypass: Mitigation; Speculative Store Bypass disabled via prctl Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointer sanitization Spectre v2: Mitigation; Enhanced / Automatic IBRS; IBPB conditional; RSB filling; PBRSB-eIBRS SW sequence; BHI BHI_DIS_S Srbds: Not affected Tsx async abort: Not affected
sudo lshw -short -C memory H/W path Device Class Description ======================================================== /0/0 memory 64KiB BIOS /0/3b memory 40GiB System Memory /0/3b/0 memory 8GiB DIMM Synchronous 4800 MHz (0,2 ns) /0/3b/1 memory 16GiB DIMM Synchronous 4800 MHz (0,2 ns) /0/3b/2 memory 16GiB DIMM Synchronous 4800 MHz (0,2 ns) /0/3b/3 memory [empty] /0/4b memory 384KiB L1 cache /0/4c memory 256KiB L1 cache /0/4d memory 10MiB L2 cache /0/4e memory 25MiB L3 cache /0/4f memory 128KiB L1 cache /0/50 memory 256KiB L1 cache /0/51 memory 2MiB L2 cache /0/52 memory 25MiB L3 cache /0/100/14.2 memory RAM memory
Если мы заглянем в pprof memory profile, то получим результат:
sync.Map Showing nodes accounting for 67752.28kB, 99.89% of 67827.20kB total flat flat% sum% cum cum% 51064.96kB 75.29% 75.29% 54407.03kB 80.21% sync.(*Map).Swap 6975.31kB 10.28% 85.57% 67752.34kB 99.89% lesson.BenchmarkSyncMapWriteMillion.func1 6373.84kB 9.40% 94.97% 6373.84kB 9.40% strconv.formatBits 3338.13kB 4.92% 99.89% 3338.13kB 4.92% sync.newEntry (inline) ==================================== Showing nodes accounting for 108636.43kB, 99.92% of 108720.77kB total flat flat% sum% cum cum% 45864.58kB 42.19% 42.19% 75216.60kB 69.18% sync.(*Map).Swap 23786.82kB 21.88% 64.06% 23786.82kB 21.88% sync.(*Map).dirtyLocked 22013.31kB 20.25% 84.31% 22013.31kB 20.25% strconv.formatBits 11164.25kB 10.27% 94.58% 86380.85kB 79.45% lesson.(*FastSyncMap).Store 5561.55kB 5.12% 99.70% 5561.55kB 5.12% sync.newEntry (inline)
FastSyncMap потребляет примерно в 1.6 раза больше памяти, чем sync.Map.
Получается, наша реализация обеспечивает лучшую производительность параллельных операций за счет увеличенного потребления памяти. Это компромисс между скоростью и использованием памяти, что может быть приемлемо в случаях, когда требуется высокая производительность и доступно достаточное количество оперативной памяти.
Идем дальше — смотрим CPU профиль
go tool pprof fastsyncmap_cpu.prof File: lesson.test Type: cpu Time: Jun 19, 2024 at 10:38am (MSK) Duration: 1s, Total samples = 3.10s (309.30%) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 2130ms, 68.71% of 3100ms total Dropped 52 nodes (cum <= 15.50ms) Showing top 10 nodes out of 80 flat flat% sum% cum cum% 440ms 14.19% 14.19% 560ms 18.06% runtime.mapaccess2 300ms 9.68% 23.87% 300ms 9.68% runtime.(*mspan).base (inline) 270ms 8.71% 32.58% 270ms 8.71% runtime.(*gcBits).bitp (inline) 270ms 8.71% 41.29% 310ms 10.00% runtime.findObject 210ms 6.77% 48.06% 210ms 6.77% aeshashbody 190ms 6.13% 54.19% 190ms 6.13% runtime.procyield 160ms 5.16% 59.35% 1380ms 44.52% runtime.gcDrain 160ms 5.16% 64.52% 1190ms 38.39% runtime.scanobject 70ms 2.26% 66.77% 70ms 2.26% sync.(*entry).tryExpungeLocked 60ms 1.94% 68.71% 60ms 1.94% runtime.(*mspan).heapBitsSmallForAddr go tool pprof syncmap_cpu.profcpu.prof File: lesson.test Type: cpu Time: Jun 19, 2024 at 10:38am (MSK) Duration: 1.01s, Total samples = 2.13s (210.73%) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 1550ms, 72.77% of 2130ms total Dropped 47 nodes (cum <= 10.65ms) Showing top 10 nodes out of 56 flat flat% sum% cum cum% 280ms 13.15% 13.15% 380ms 17.84% runtime.mapaccess2 270ms 12.68% 25.82% 270ms 12.68% runtime.procyield 210ms 9.86% 35.68% 210ms 9.86% runtime.(*mspan).base (inline) 190ms 8.92% 44.60% 190ms 8.92% runtime.(*gcBits).bitp (inline) 140ms 6.57% 51.17% 150ms 7.04% runtime.findObject 130ms 6.10% 57.28% 130ms 6.10% aeshashbody 100ms 4.69% 61.97% 810ms 38.03% runtime.scanobject 90ms 4.23% 66.20% 230ms 10.80% runtime.mallocgc 80ms 3.76% 69.95% 80ms 3.76% runtime.markBits.isMarked (inline) 60ms 2.82% 72.77% 60ms 2.82% runtime.nextFreeFast (inline)
-
Общее время выполнения:
-
FastSyncMapиспользовал 3100ms CPU. -
sync.Mapиспользовал 2130ms CPU.
-
-
Основные потребители CPU:
-
В обоих случаях значительное количество времени тратится на функцию
runtime.mapaccess2, которая отвечает за доступ к элементам карты (sync.Mapи, предположительно, внутренние структурыFastSyncMap).
-
-
Гарbage Collection и управление памятью:
-
gcDrainиscanobjectтакже занимают значительное время в обоих профилях, указывая на активное участие сборщика мусора (GC). -
runtime.procyieldиaeshashbodyтакже занимают значительное время, что может указывать на задержки и вычисления, связанные с хешированием и управлением ресурсами.
-
Идем дальше — смотрим профиль блокировок
go tool pprof fastsyncmap_block.prof File: lesson.test Type: delay Time: Jun 19, 2024 at 10:40am (MSK) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 3122.65ms, 100% of 3122.65ms total Dropped 2 nodes (cum <= 15.61ms) Showing top 10 nodes out of 14 flat flat% sum% cum cum% 2076.23ms 66.49% 66.49% 2076.23ms 66.49% sync.(*WaitGroup).Wait 703.08ms 22.52% 89.00% 703.08ms 22.52% sync.(*Mutex).Lock (inline) 343.34ms 11.00% 100% 343.34ms 11.00% runtime.chanrecv1 0 0% 100% 702.53ms 22.50% lesson.(*FastSyncMap).Store 0 0% 100% 2419.57ms 77.48% lesson.BenchmarkFastSyncMapWriteMillion 0 0% 100% 703.08ms 22.52% lesson.BenchmarkFastSyncMapWriteMillion.func1 0 0% 100% 343.34ms 11.00% runtime/pprof.StopCPUProfile 0 0% 100% 702.53ms 22.50% sync.(*Map).Store (inline) 0 0% 100% 702.53ms 22.50% sync.(*Map).Swap 0 0% 100% 2076.23ms 66.49% testing.(*B).RunParallel go tool pprof syncmap_block.prof File: lesson.test Type: delay Time: Jun 19, 2024 at 10:40am (MSK) Entering interactive mode (type "help" for commands, "o" for options) (pprof) top Showing nodes accounting for 7777.40ms, 100% of 7777.40ms total Dropped 2 nodes (cum <= 38.89ms) Showing top 10 nodes out of 26 flat flat% sum% cum cum% 3550.66ms 45.65% 45.65% 3550.66ms 45.65% sync.(*WaitGroup).Wait 3310.10ms 42.56% 88.21% 3310.10ms 42.56% runtime.chanrecv1 916.65ms 11.79% 100% 916.65ms 11.79% sync.(*Mutex).Lock (inline) 0 0% 100% 702.53ms 9.03% lesson.(*FastSyncMap).Store 0 0% 100% 2609.71ms 33.56% lesson.BenchmarkFastSyncMapWriteMillion 0 0% 100% 703.08ms 9.04% lesson.BenchmarkFastSyncMapWriteMillion.func1 0 0% 100% 1626.74ms 20.92% lesson.BenchmarkSyncMapWriteMillion 0 0% 100% 213.57ms 2.75% lesson.BenchmarkSyncMapWriteMillion.func1 0 0% 100% 2624.31ms 33.74% main.main 0 0% 100% 2624.31ms 33.74% runtime.main
Анализ результатов
-
Общее время задержек:
-
FastSyncMap: 3122.65 ms -
sync.Map: 7777.40 ms
-
sync.Map имеет значительно большее общее время задержек по сравнению с FastSyncMap.
-
Основные узлы задержек:
-
В
FastSyncMapосновное время задержек приходится наsync.(*WaitGroup).Wait(66.49%) иsync.(*Mutex).Lock(22.52%). -
В
sync.Mapосновное время задержек также приходится наsync.(*WaitGroup).Wait(45.65%) иruntime.chanrecv1(42.56%).
-
Объяснение
-
Время ожидания WaitGroup:
-
Оба метода используют
sync.WaitGroupдля синхронизации горутин, что приводит к значительному времени ожидания. ВFastSyncMapэто время составляет 2076.23 ms (66.49%), в то время как вsync.Mapэто время составляет 3550.66 ms (45.65%).
-
-
Время ожидания блокировок Mutex:
-
В
FastSyncMapблокировкиsync.Mutexвызывают задержки на 703.08 ms (22.52%), в то время как вsync.Mapзадержки из-заsync.Mutexсоставляют 916.65 ms (11.79%).
-
-
Время ожидания каналов:
-
В
sync.Mapзначительное время (3310.10 ms, 42.56%) уходит на ожидание в каналах, что может быть связано с особенностями реализации и конкурентным доступом к элементам карты.
-
Заключение
-
FastSyncMap:-
Имеет меньшее общее время задержек (3122.65 ms) по сравнению с
sync.Map. -
Основные задержки связаны с
sync.WaitGroup(66.49%) иsync.Mutex(22.52%).
-
-
sync.Map:-
Имеет большее общее время задержек (7777.40 ms).
-
Основные задержки связаны с
sync.WaitGroup(45.65%) и ожиданием в каналах (42.56%).
-
ссылка на оригинал статьи https://habr.com/ru/articles/822861/
Добавить комментарий