Historia pytania. Mapa w Go jest zaimplementowana jako tabela haszująca z możliwością rozrostu kubków. Kiedy współczynnik obciążenia przekracza określony próg, czas wykonania inicjuje fazę wzrostu, w której wpisy są ponownie haszowane i redistribuowane do nowych, większych tablic kubków.
Problem. Gdyby język pozwalał na wyrażenia takie jak &m["klucz"], powstałyby wskaźniki odnoszące się do konkretnej lokalizacji pamięci wewnątrz kubka haszującego. W trakcie wzrostu mapy wpisy są kopiowane do nowych kubków, a stare kubki są zwalniane, co sprawia, że wszelkie istniejące wskaźniki stają się nieaktualne i niebezpieczne.
Rozwiązanie. Specyfikacja Go wyraźnie zabrania pobierania adresu z wyrażenia indeksu mapy. Kompilator traktuje &m[k] jako nieprawidłową operację, zapewniając, że żaden program nie może utrzymywać wskaźników do wnętrza mapy. Pozwala to na swobodne przenoszenie wpisów podczas wzrostu lub zmniejszania bez konieczności zarządzania aktualizacjami wskaźników lub ich unieważnianiem.
Opis problemu. W usłudze telemetrycznej o wysokiej przepustowości inżynierowie musieli zaktualizować pole licznika w dużej strukturze konfiguracyjnej przechowywanej w mapie pamięci podręcznej. Początkowe próby użycia cfg := &configMap[deviceID]; cfg.Counter++ zakończyły się błędem kompilacji: "nie można pobrać adresu elementu mapy". Taki schemat był powszechny w bazach kodu C++, z których zespół migrował, gdzie iteratory std::map pozostają stabilne.
Rozwiązanie 1: Przechowuj wskaźniki w mapie. Zmień typ mapy z map[string]Config na map[string]*Config. Umożliwia to pobranie wskaźnika i bezpośrednią modyfikację struktury bazowej bez ponownego przypisania. Zalety to możliwość bezpośredniej modyfikacji i unikanie kopiowania struktur, a wady to zwiększone przydziały na stercie, zmniejszona lokalność pamięci podręcznej i konieczność sprawdzania nil.
Rozwiązanie 2: Kopiuj-modyfikuj-przypisuj. Pobierz wartość do zmiennej lokalnej, zmodyfikuj ją i zapisz z powrotem używając cfg := configMap[deviceID]; cfg.Counter++; configMap[deviceID] = cfg. Zalety to praca z typami wartości i z zerowymi dodatkowymi przydziałami, a wady to narzut wydajności związany z kopiowaniem dużych struktur przy każdej aktualizacji.
Rozwiązanie 3: Użyj sync.RWMutex z opakowaniem strukturalnym. Chroń mapę za pomocą muteksu, aby umożliwić bezpieczne cykle odczytu-modyfikacji-zapisu w środowiskach współbieżnych. Zalety to jawna synchronizacja dla współbieżnego dostępu, a wady to potencjalne konflikty blokad i wciąż konieczność ponownego przypisywania.
Wybrane rozwiązanie i wynik. Dla małych struktur (<64 bajtów) przyjęto rozwiązanie 2 ze względu na jego prostotę i właściwości zero-przydziałowe. Dla dużych, często aktualizowanych struktur zastosowano rozwiązanie 1 z alokacją puli, aby złagodzić presję GC. System osiągnął stabilną wydajność bez polegania na niebezpiecznych sztuczkach ze wskaźnikami.
Dlaczego mogę pobrać adres elementu slice, ale nie elementu mapy?
Pobieranie adresu elementu slice &s[i] jest ważne, ponieważ tablica bazowa slice ma stabilny adres w pamięci, chyba że slice zostanie ponownie przydzielony (np. poprzez append, przekraczając pojemność). Wskaźnik pozostaje ważny tak długo, jak długo tablica bazowa nie jest ponownie przydzielana. W przeciwieństwie do tego, kubki mapy są rutynowo przenoszone podczas operacji wzrostu. Jeśli pozwolono by na adresy elementów mapy, stałyby się one wskaźnikami umarłymi po ponownym haszowaniu, naruszając bezpieczeństwo pamięci.
Czy użycie mapy wskaźników pozwala na modyfikację przechowywanych danych bez ponownego przypisywania?
Chociaż nie można pobrać adresu samego miejsca na wskaźnik (&m[key] jest nieprawidłowe nawet dla map[K]*V), możesz skopiować wartość wskaźnika i zdereferować ją: p := m[key]; p.Field = newVal. Działa to, ponieważ modyfikujesz strukturę alokowaną na stercie w wyniku kopiowania wskaźnika, a nie wewnętrznego magazynu mapy. Różnica jest subtelna: mapa przechowuje wartość wskaźnika (adres), a chociaż wartość adresu nie może być adresowana bezpośrednio, można ją odczytać i używać do uzyskania dostępu do obiektu na stercie.
Jak działałby wzrost mapy, gdyby dozwolone były adresy elementów?
Gdyby język zezwalał na &m[key], czas wykonania musiałby zapewnić stabilność wskaźników podczas migracji kubków. Wymagałoby to albo pośrednictwa (przechowywanie wskaźników do wpisów w kubkach, podwajając obciążenie wskaźników), nigdy nie zwalniania starych kubków (wyciek pamięci), albo zaimplementowania bariery odczytu w celu aktualizacji wskaźników podczas przenoszenia (znaczny koszt wydajności). Obecny projekt optymalizuje dla typowego przypadku operacji mapy, poświęcając zdolność do pobierania adresów elementów, unikając tych narzutów.