Sorunun tarihi. Go'nun haritası, büyüyebilen çuvallara sahip bir hash tablosu olarak uygulanmıştır. Yük faktörü bir eşik değerini aştığında, çalışma zamanı bir büyüme aşamasını başlatır; burada kayıtlı veriler yeniden hashlenir ve yeni, daha büyük çuval dizilerine dağıtılır.
Sorun. Eğer dil &m["anahtar"] gibi ifadeleri izin verseydi, sonuçta oluşan işaretçi, bir hash çuvalındaki belirli bir bellek alanını referans alırdı. Harita büyürken, kayıtlı veriler yeni çuvallara kopyalanır ve eski çuvallar serbest bırakılır, bu da mevcut işaretçilerin dangling hâle gelmesine ve güvensiz olmasına yol açar.
Çözüm. Go spesifikasyonu açıkça bir harita dizin ifadesinin adresini almayı yasaklar. Derleyici &m[k] ifadesini geçersiz bir işlem olarak değerlendirir ve bu sayede hiçbir program haritanın iç yapılarına işaretçi tutamaz. Bu, çalışma zamanının büyüme veya küçülme sırasında kayıtların serbestçe yer değiştirmesine olanak tanır; bunun yanı sıra işaretçi güncellemelerini veya geçersiz kılmayı yönetmek zorunda kalmaz.
Sorun açıklaması. Yüksek verimli bir telemetri hizmetinde mühendisler, bellekte saklanan büyük bir yapılandırma yapısının içindeki bir sayaç alanını güncellemek zorunda kaldılar. İlk denemeler cfg := &configMap[deviceID]; cfg.Counter++ kullandı ve bu, "harita öğesinin adresini alamaz" hatası ile derlenmedi. Bu desen, ekiplerinin göç ettiği C++ kod tabanlarında sık görülüyordu; burada std::map iteratörleri kararlı kalıyordu.
Çözüm 1: Haritada işaretçi saklamak. Harita türünü map[string]Config yerine map[string]*Config olarak değiştirin. Bu, işaretçiyi almayı ve yapılandırma yapısını doğrudan değiştirmeyi mümkün kılar. Artıları arasında doğrudan değişiklik yapma olanağı ve yapı kopyalamaktan kaçınma sayılabilirken, eksileri arasında artan y heap tahsisleri, azalan önbellek yerelliği ve nil kontrollerinin gerekliliği yer alır.
Çözüm 2: Kopyala-değiştir-yeniden atama. Değeri yerel bir değişkende al, değiştir ve cfg := configMap[deviceID]; cfg.Counter++; configMap[deviceID] = cfg kullanarak geri yaz. Artıları arasında değer türleriyle çalışma ve sıfır ek tahsis bulunması, eksileri arasında her güncellemede büyük yapıların kopyalanması nedeniyle performans üzerindeki yük sayılabilir.
Çözüm 3: Bir yapı sarmalayıcı ile sync.RWMutex kullanma. Haritayı bir mutex ile koruyarak, eşzamanlı ortamlarda güvenli okuma-değiştirme-yazma döngülerine olanak tanır. Artıları arasında eşzamanlı erişim için açık senkronizasyon sayılabilirken, eksileri arasında potansiyel kilit içerikleri ve yeniden atama gerekliliği bulunur.
Seçilen çözüm ve sonuç. Küçük yapılar için (<64 bayt), basitliği ve sıfır tahsis özellikleri nedeniyle Çözüm 2 benimsenmiştir. Sık güncellenen büyük yapılar için, GC baskısını hafifletmek amacıyla Çözüm 1 kullanıldı. Sistem, güvensiz işaretçi hack'lerine dayanmadan kararlı bir performans elde etti.
Neden bir dilim öğesinin adresini alabiliyorum ama bir harita öğesininki değil?
Bir dilim öğesinin adresini &s[i] almak geçerli çünkü bir dilimin arka ucu sabit bir bellek adresine sahiptir, eğer dilim yeniden tahsis edilmediği sürece (örneğin, kapasiteyi aşan bir append ile). İşaretçi, arka ucu yeniden tahsis edilmediği sürece geçerliliğini korur. Aksine, harita çuvalları büyüme işlemleri sırasında düzenli olarak yeniden yerleştirilir. Eğer harita öğelerinin adreslerinin alınmasına izin verilseydi, bunlar yeniden hash işlemi sonrası dangling işaretçiler haline gelir, bu da bellek güvenliğini ihlal eder.
Bir işaretçi haritası kullanmak, saklanan veriyi yeniden atama olmadan değiştirmeyi sağlar mı?
İşaretçi slotunun adresini alamazsınız (&m[key] geçersizdir, map[K]*V için bile), ancak işaretçi değerini dışarı kopyalayabilir ve onu derefanslayabilirsiniz: p := m[key]; p.Field = newVal. Bu, işaretçi kopyası aracılığıyla heap'te tahsis edilen yapıyı değiştirdiğiniz için çalışır, haritanın iç depolaması değil. Ayrım ince: harita işaretçi değerini (bir adres) saklar, ve o adres değeri doğrudan alınaamaz, ama okunabilir ve heap nesnesine erişmek için kullanılabilir.
Eğer öğelerin adresleri alına bilseydi harita büyümesi nasıl olurdu?
Eğer dil &m[key] izin verseydi, çalışma zamanının çuval göçü sırasında işaretçi kararlılığını sağlamak için garanti etmesi gerekirdi. Bu, ya dolaylılık gerektirir (çuvallarda kaydedilmiş kayıtların işaretçilerini saklamak, işaretçi yükünü iki katına çıkartmak), eski çuvalları serbest bırakmamayı (bellek sızıntısı), ya da taşınma işlemi sırasında işaretçileri güncellemek için bir okuma engeli uygulamayı (önemli performans maliyeti) gerektirirdi. Mevcut tasarım, işaretçi adreslerini alabilme yeteneğini feda ederek harita işlemlerinin yaygın durumları için optimize eder ve bu yüklerden kaçınır.