Grootte-gespecialiseerde geheugenallocatie

Go 1.27 bevat snellere geheugenallocatie voor allocaties van 80 bytes of minder. Allocaties kunnen tot 20-30% sneller zijn, waardoor programma's die veel alloceren tot 1% sneller worden.

De Go-runtime verbetert de prestaties van deze allocaties door gespecialiseerde functies toe te voegen die worden gebruikt om bepaalde groottes te alloceren. Deze gespecialiseerde functies kunnen bepaalde aannames doen, waardoor ze sneller zijn en gemakkelijker te optimaliseren. Dit artikel legt uit hoe dit werkt en hoe het uw programma's versnelt.

Hoe heap-allocaties werken

Heap-allocaties worden aangemaakt door de mallocgc-functie van de runtime, die de grootte van de allocatie en de vraag of deze pointers bevat, vereist. Wanneer de compiler vaststelt dat een object ontsnapt naar de heap (escapes to the heap) of anderszins dynamisch moet worden gealloceerd, voegt deze een aanroep toe aan newobject. Dit is een eenvoudige wrapper-functie die de grootte van het object en de aanwezigheid van pointers extraheert en deze doorgeeft aan mallocgc.

Deze twee stukken informatie bepalen het grootste deel van het werk dat de allocator moet verrichten. De grootte is cruciaal omdat de allocator bereiken van groottes definieert, genaamd "size classes" (grootteklassen). Voor de meeste allocaties die niet te groot en niet te klein zijn, wordt een blok geheugen geretourneerd uit een lijst met vrije objecten die allemaal de maximale grootte van die grootteklasse hebben.

Voorbeeld: Grootteklasse 3 beslaat 17-24 bytes. Of u nu 17 bytes of 24 bytes allocatert, de allocator geeft u het volgende beschikbare object van 24 bytes uit zijn lijst met 24-byte objecten.

Overzicht van grootteklassen tot 80 bytes

GrootteklasseBereik van groottes
11-8 bytes
29-16 bytes
317-24 bytes
425-32 bytes
533-48 bytes
649-64 bytes
765-80 bytes

Er zijn aparte sets van vrije lijsten, die we "spans" noemen, afhankelijk van of de allocatie pointers bevat. Dit is noodzakelijk voor de administratie die de garbage collector (GC) moet bijhouden.

De implementatie van gespecialiseerde allocatie

Omdat de grootteklasse en de aanwezigheid van pointers bepalen uit welke span er wordt gealloceerd, gebruikt de allocator een type genaamd de "span class". De waarde hiervan is gedefinieerd als sizeClass<<1 | noPointers. Bij het implementeren van grootte-gespecialiseerde malloc bleek het het meest logisch om te specialiseren op basis van deze span-klassen.

Er is een gespecialiseerde mallocgc-variant gegenereerd voor elke span-klasse. Een voorbeeld hiervan is de functie mallocgcSmallNoScanSC3, die pointer-vrije objecten in grootteklasse 3 allocatert. Hierbij staat:

  • Small: niet "tiny" of "large".
  • NoScan: bevat geen pointers.
  • SC3: grootteklasse 3 (17-24 bytes).

Gespecialiseerde functies zijn zo eenvoudig mogelijk ontworpen en kunnen niet alle uitzonderingsgevallen afhandelen. Wanneer ze zo'n geval detecteren (bijvoorbeeld wanneer de GC actief is), vallen ze terug op een meer generieke allocatieroutine.

Optimalisaties en beperkingen

Dynamische aanroepen en de instructiecache

De compiler weet vaak niet welke span-klasse wordt gealloceerd tijdens het genereren van de code (denk aan slices met een dynamische lengte). In dat geval blijft de aanroep naar mallocgc staan, waarna mallocgc zelf moet bepalen of er een gespecialiseerde functie beschikbaar is en deze vervolgens aanroept. De prestatieverbetering moet dus groot genoeg zijn om ook de overhead van deze dynamische aanroep te compenseren.

Daarnaast neemt elke toegevoegde gespecialiseerde functie ruimte in beslag in de executables en, belangrijker nog, in de instructiecache (icache). De enkele mallocgc-functie bevindt zich vaak in de cache vanwege de frequentie waarmee allocaties plaatsvinden. Te veel gespecialiseerde functies kunnen de cache overbelasten, waardoor user-code wordt weggedrukt en het ophalen van code trager wordt. Na diverse benchmarks is bepaald dat stoppen bij 80 bytes het optimale punt ("sweet spot") is.

Onderhoud via code-generatie

Om te voorkomen dat handgeschreven gespecialiseerde functies uit sync raken, zijn de gemeenschappelijke delen van de functies gescheiden. Er is een inliner geschreven die gebruikmaakt van de standaardbibliotheek go/ast voor het parsen en formatteren, en het golang.org/x/tools/go/ast/astutil pakket voor het manipuleren van de AST's (Abstract Syntax Trees).

Waarom zijn deze functies sneller?

De belangrijkste optimalisaties zijn:

  1. Efficiënter wissen van geheugen: Geheugen geretourneerd door mallocgc moet vaak worden genuld. De functie memclrNoHeapPointers is geschreven in hoog-geoptimaliseerde assembly, maar voor kleine wissen kan de compiler calls naar deze functie vervangen door directe instructies om het geheugen te wissen. Dit bespaart functieaanroepen en vertakkingen (branches).
  2. Minder berekeningen: Omdat functies per span-klasse zijn gespecialiseerd, hoeft de functie de span-klasse niet opnieuw te berekenen bij het ophalen van de span.
  3. Constante groottes: Omdat de allocatiegrootte een constante is, kan de compiler de administratie (boekhouding) versnellen, zoals het markeren waar de pointers in het gealloceerde geheugen zich bevinden.
  4. Handmatige inlining: De generator kan functielichamen direct in de aanroeper plaatsen, waardoor de beperkingen van de Go-compiler wat betreft de grootte van inlined functies worden omzeild. Minder voorkomende gevallen (zoals runtime-debugvlaggen) zijn verplaatst naar "slow path"-functies om de gespecialiseerde functies klein te houden.

Praktische impact voor de programmeur

Als Go-programmeur hoeft u niets aan te passen in uw code. Geheugenallocaties zullen simpelweg sneller zijn. De grootste voordelen zijn merkbaar bij de meest voorkomende allocatiegroottes, specifiek de 16- en 24-byte allocaties. Deze komen veel voor omdat ze bestaan uit twee of drie 64-bit waarden; denk hierbij aan interface-waarden en strings (twee waarden) of slices (drie waarden).

Hoe te gebruiken of uit te schakelen

Om te profiteren van deze verbeteringen, hoeft u uw programma's enkel te compileren met Go 1.27. Voor meer informatie over het verbeteren van geheugenallocatie en garbage collection prestaties, wordt verwezen naar de Go Garbage Collector Optimization Guide.

Hoewel we er vertrouwen op dat deze wijziging geen regressies veroorzaakt, kunt u de grootte-gespecialiseerde allocatie indien nodig uitschakelen door uw programma's te bouwen met:

GOEXPERIMENT=nosizespecializedmalloc

Mocht u problemen ervaren waardoor u dit moet uitschakelen, meld dit dan via go.dev/issue/new zodat het team dit kan onderzoeken.