Go-Zuweisungsmechanismen

Als ich zum ersten Mal versuchte zu verstehen, wie die Speicherzuweisungswerkzeuge in Go funktionieren, schien mir das, was ich herausfinden wollte, wie eine mysteriöse Black Box. Wie bei jeder anderen Technologie ist das Wichtigste hier hinter vielen Abstraktionsebenen verborgen, durch die Sie hindurch müssen, um etwas zu verstehen.



Der Autor des Materials, dessen Übersetzung wir veröffentlichen, hat beschlossen, die Mittel zur Speicherzuweisung in Go auf den Grund zu gehen und darüber zu sprechen.

Physischer und virtueller Speicher


Alle Mittel zum Zuweisen von Speicher müssen mit dem Adressraum des virtuellen Speichers arbeiten, der vom Betriebssystem gesteuert wird. Lassen Sie uns einen Blick darauf werfen, wie Speicher funktioniert, beginnend auf der untersten Ebene - mit Speicherzellen.
So stellen Sie sich eine RAM-Zelle vor.


Speicherzellenlayout

Wenn wir uns auf sehr vereinfachte Weise eine Speicherzelle vorstellen und was sie umgibt, erhalten wir Folgendes:

  1. Die Adressleitung (der Transistor fungiert als Schalter) ermöglicht den Zugang zum Kondensator (Datenleitung).
  2. Wenn ein Signal in der Adresszeile (rote Linie) erscheint, können Sie über die Datenleitung Daten in die Speicherzelle schreiben, dh den Kondensator aufladen, wodurch ein logischer Wert entsprechend 1 darin gespeichert werden kann.
  3. Wenn in der Adressleitung (grüne Leitung) kein Signal vorhanden ist, ist der Kondensator isoliert und seine Ladung ändert sich nicht. Um in Zelle 0 zu schreiben, müssen Sie ihre Adresse auswählen und eine logische 0 über die Datenleitung senden, dh die Datenleitung mit einem Minus verbinden, wodurch der Kondensator entladen wird.
  4. Wenn der Prozessor den Wert aus dem Speicher lesen muss, wird das Signal entlang der Adressleitung gesendet (der Schalter schließt). Wenn der Kondensator geladen ist, geht das Signal durch die Datenleitung (1 wird gelesen), andernfalls geht das Signal nicht durch die Datenleitung (0 wird gelesen).


Das Schema der Interaktion von physischem Speicher und Prozessor

Der Datenbus ist für den Transport von Daten zwischen dem Prozessor und dem physischen Speicher verantwortlich.

Lassen Sie uns nun über die Adresszeile und die adressierbaren Bytes sprechen.


Busadressleitungen zwischen Prozessor und physischem Speicher

  1. Jedem Byte im RAM wird eine eindeutige numerische Kennung (Adresse) zugewiesen. Es ist zu beachten, dass die Anzahl der im Speicher vorhandenen physischen Bytes nicht gleich der Anzahl der Adressleitungen ist.
  2. Jede Adresszeile kann einen 1-Bit-Wert angeben, sodass ein Bit in der Adresse eines bestimmten Bytes angegeben wird.
  3. Unsere Schaltung hat 32 Adressleitungen. Infolgedessen verwendet jedes adressierbare Byte eine 32-Bit-Nummer als Adresse. [00000000000000000000000000000000] - die niedrigste Speicheradresse. [11111111111111111111111111111111] - die höchste Speicheradresse.
  4. Da jedes Byte eine 32-Bit-Adresse hat, besteht unser Adressraum aus 2 32 adressierbaren Bytes (4 GB).

Als Ergebnis stellt sich heraus, dass die Anzahl der adressierbaren Bytes von der Gesamtzahl der Adressleitungen abhängt. Wenn beispielsweise 64 Adressleitungen (x86-64-Prozessoren) vorhanden sind, können Sie 2 64 Byte (16 Exabyte) Speicher adressieren. Die meisten Architekturen, die 64-Bit-Zeiger verwenden, verwenden jedoch tatsächlich 48-Bit-Adressleitungen (AMD64). und 42-Bit-Adressleitungen (Intel), mit denen Computer theoretisch mit 256 Terabyte physischem Speicher ausgestattet werden können (Linux ermöglicht auf der x86-64-Architektur bei Verwendung von Adressseiten der Ebene 4, Prozessen Prozesse mit bis zu 128 TB Adressraum zuzuweisen, mit Windows können Sie bis zu 128 TB Adressraum zuweisen 192 TB).
Da die Größe des physischen Arbeitsspeichers begrenzt ist, wird jeder Prozess in einer eigenen "Sandbox" ausgeführt - im sogenannten "virtuellen Adressraum", dem so genannten virtuellen Speicher.

Die Byteadressen im virtuellen Adressraum stimmen nicht mit den Adressen überein, die der Prozessor für den Zugriff auf den physischen Speicher verwendet. Daher benötigen wir ein System, mit dem wir virtuelle Adressen in physische Adressen konvertieren können. Sehen Sie sich an, wie Adressen des virtuellen Speichers aussehen.


Darstellung des virtuellen Adressraums

Wenn der Prozessor einen Befehl ausführt, der sich auf eine Speicheradresse bezieht, besteht der erste Schritt darin, die logische Adresse in eine lineare Adresse zu übersetzen. Diese Konvertierung wird von der Speicherverwaltungseinheit durchgeführt.


Vereinfachte Darstellung der Beziehung zwischen virtuellem und physischem Speicher

Da logische Adressen zu groß sind, um separat damit arbeiten zu können (dies hängt von verschiedenen Faktoren ab), ist der Speicher in Strukturen organisiert, die als Seiten bezeichnet werden. In diesem Fall ist der virtuelle Adressraum in kleine Bereiche unterteilt, Seiten, die in den meisten Betriebssystemen 4 KB groß sind, obwohl diese Größe normalerweise geändert werden kann. Dies ist die kleinste Einheit der Speicherverwaltung im virtuellen Speicher. Der virtuelle Speicher speichert nichts, sondern stellt lediglich die Entsprechung zwischen dem Adressraum des Programms und dem physischen Speicher ein.

Prozesse sehen nur Adressen des virtuellen Speichers. Was passiert, wenn ein Programm mehr dynamischen Speicher benötigt (auch Heapspeicher oder „Heap“ genannt)? Hier ist ein Beispiel für einfachen Assembler-Code, in dem zusätzlicher dynamisch zugewiesener Speicher vom System angefordert wird:

_start:        mov $12, %rax #    brk        mov $0, %rdi # 0 -  ,            syscall b0:        mov %rax, %rsi #  rsi    ,           mov %rax, %rdi #     ...        add $4, %rdi # ..  4 ,           mov $12, %rax #    brk        syscall 

So kann es in Form eines Diagramms dargestellt werden.


Erhöhen Sie den dynamisch zugewiesenen Speicher

Das Programm fordert über den brk -Systemaufruf (sbrk / mmap usw.) zusätzlichen Speicher an. Der Kernel aktualisiert die Informationen zum virtuellen Speicher, aber neue Seiten wurden noch nicht im physischen Speicher angezeigt, und hier gibt es einen Unterschied zwischen virtuellem und physischem Speicher.

Speicherzuordnung


Nachdem wir allgemein über die Arbeit mit dem virtuellen Adressraum gesprochen und darüber gesprochen haben, wie zusätzlicher dynamischer Speicher (Speicher auf dem Heap) angefordert werden kann, wird es für uns einfacher sein, über Mittel zum Zuweisen von Speicher zu sprechen.

Wenn der Heap über genügend Speicher verfügt, um unsere Codeanforderungen zu erfüllen, kann der Speicherzuweiser diese Anforderungen ausführen, ohne auf den Kernel zuzugreifen. Andernfalls muss er den Heap mithilfe eines Systemaufrufs (z. B. brk) vergrößern und gleichzeitig einen großen Speicherblock anfordern. Im Fall von malloc bedeutet "groß" die durch den Parameter MMAP_THRESHOLD Größe, die standardmäßig 128 MMAP_THRESHOLD beträgt.

Ein Speicherzuweiser hat jedoch mehr Verantwortlichkeiten als nur das Zuweisen von Speicher. Eine seiner wichtigsten Aufgaben ist es, die interne und externe Speicherfragmentierung zu reduzieren und Speicherblöcke so schnell wie möglich zuzuweisen. Angenommen, unser Programm führt nacheinander Anforderungen zum Zuweisen fortlaufender Speicherblöcke unter Verwendung einer Funktion des Formulars malloc(size) , wonach dieser Speicher mithilfe einer Funktion des Formulars free(pointer) freigegeben wird.


Demonstration der externen Fragmentierung

Im vorherigen Diagramm haben wir in Schritt p4 nicht genügend sequenziell angeordnete Speicherblöcke, um die Anforderung der Zuweisung von sechs solchen Blöcken zu erfüllen, obwohl die Gesamtmenge an freiem Speicher dies zulässt. Diese Situation führt zu einer Speicherfragmentierung.

Wie kann die Speicherfragmentierung reduziert werden? Die Antwort auf diese Frage hängt vom spezifischen Speicherzuweisungsalgorithmus ab, auf dem die Basisbibliothek für die Arbeit mit dem Speicher verwendet wird.

Jetzt schauen wir uns das TCMalloc-Speicherzuweisungstool an, auf dem die Go-Speicherzuweisungsmechanismen basieren.

TCMalloc


TCMalloc basiert auf der Idee, den Speicher in mehrere Ebenen zu unterteilen, um die Speicherfragmentierung zu verringern. In TCMalloc ist die Speicherverwaltung in zwei Teile unterteilt: Arbeiten mit dem Thread-Speicher und Arbeiten mit dem Heap.

▍ Thread-Speicher


Jede Speicherseite ist in eine Folge von Fragmenten bestimmter Größen unterteilt, die gemäß den Größenklassen ausgewählt werden. Dies reduziert die Fragmentierung. Infolgedessen verfügt jeder Thread über einen Cache für kleine Objekte, der eine sehr effiziente Speicherzuweisung für Objekte mit einer Größe von 32 KB oder weniger ermöglicht.


Stream-Cache

»Bündel


Ein von TCMalloc verwalteter Heap ist eine Sammlung von Seiten, in denen eine Reihe aufeinanderfolgender Seiten als Seitenbereich (Bereich) dargestellt werden kann. Wenn Sie Speicher für ein Objekt zuweisen müssen, das größer als 32 KB ist, wird Heap zum Zuweisen von Speicher verwendet.


Haufen und arbeiten mit Seiten

Wenn nicht genügend Speicherplatz vorhanden ist, um kleine Objekte im Speicher abzulegen, wenden sie sich an den Heap für den Speicher. Wenn der Heap nicht über genügend freien Speicher verfügt, wird vom Betriebssystem zusätzlicher Speicher angefordert.

Infolgedessen unterstützt das vorgestellte Modell der Arbeit mit Speicher den Speicherpool für den Benutzerbereich, da seine Verwendung die Effizienz beim Zuweisen und Freigeben von Speicher erheblich verbessert.

Es ist zu beachten, dass das Go-Speicherzuweisungstool ursprünglich auf TCMalloc basierte, sich jedoch geringfügig davon unterscheidet.

Gehen Sie Speicherzuordnung


Wir wissen, dass die Go-Laufzeit plant, Goroutinen auf logischen Prozessoren auszuführen. In ähnlicher Weise unterteilt die von Go verwendete Version von TCMalloc Speicherseiten in Blöcke, deren Größe bestimmten Größenklassen entspricht, von denen 67 existieren.

Wenn Sie mit dem Go-Scheduler nicht vertraut sind , können Sie hier darüber lesen.


Gehen Sie Größenklassen

Da die minimale Seitengröße in Go 8192 Bytes (8 KB) beträgt, erhalten wir 8 solcher Blöcke, wenn eine solche Seite in Blöcke von 1 KB unterteilt ist.


Eine Seitengröße von 8 KB ist in Blöcke unterteilt, die einer Klassengröße von 1 KB entsprechen

Ähnliche Seitenfolgen in Go werden mithilfe einer Struktur namens mspan gesteuert.

▍Struktur mspan


Die mspan-Struktur ist eine doppelt verknüpfte Liste, ein Objekt, das die Startadresse der Seite, Informationen zur Seitengröße und die Anzahl der darin enthaltenen Seiten enthält.


Mspan Struktur

▍ mcache-Struktur


Wie TCMalloc stellt Go jedem logischen Prozessor einen lokalen Thread-Cache zur Verfügung, der als mcache bezeichnet wird. Wenn Goroutine Speicher benötigt, kann er diesen direkt aus mcache abrufen. Dazu müssen Sie keine Sperren durchführen, da zu einem bestimmten Zeitpunkt nur ein Goroutin auf einem logischen Prozessor ausgeführt wird.

Die mcache-Struktur enthält in Form eines Caches mspan-Strukturen verschiedener Größenklassen.


Interaktion zwischen logischem Prozessor, mcache und mspan in Go

Da jeder logische Prozessor über einen eigenen mcache verfügt, sind beim Zuweisen von Speicher aus mcache keine Sperren erforderlich.

Jede Größenklasse kann durch eines der folgenden Objekte dargestellt werden:

  • Ein Scanobjekt ist ein Objekt, das einen Zeiger enthält.
  • Ein Noscan-Objekt ist ein Objekt, in dem es keinen Zeiger gibt.

Eine der Stärken dieses Ansatzes besteht darin, dass Noscan-Objekte bei der Speicherbereinigung nicht umgangen werden müssen, da sie keine Objekte enthalten, denen Speicher zugewiesen ist.

Was kommt in mcache? Objekte, deren Größe 32 KB nicht überschreitet, werden mit mspan der entsprechenden Größenklasse direkt an mcache übergeben.

Was passiert, wenn mcache keine freie Zelle hat? Dann erhalten sie einen neuen mspan der gewünschten Größenklasse aus der Liste der mspan-Objekte mit dem Namen mcentral.

▍Zentrale Struktur


Die zentrale Struktur sammelt alle Seitenbereiche einer bestimmten Größenklasse. Jedes mcentral-Objekt enthält zwei Listen von mspan-Objekten.

  1. Liste der mspan-Objekte, in denen keine freien Objekte vorhanden sind, oder der mspan-Objekte, die sich im mcache befinden.
  2. Liste der mspan-Objekte mit freien Objekten.


Mcentrale Struktur

Jede zentrale Struktur existiert innerhalb der mheap-Struktur.

HeaHaufenstruktur


Die mheap-Struktur wird durch ein Objekt dargestellt, das die Heap-Verwaltung in Go übernimmt. Es gibt nur ein solches globales Objekt, das einen virtuellen Adressraum besitzt.


Mheap Struktur

Wie Sie dem obigen Diagramm entnehmen können, enthält die mheap-Struktur ein Array von mcentral-Strukturen. Dieses Array enthält zentrale Strukturen für alle Größenklassen.

 central [numSpanClasses]struct { mcentral mcentral   pad     [sys.CacheLineSize unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte } 

Da wir für jede Größenklasse eine mcentral-Struktur haben, wird auf mcentral-Ebene eine Sperre angewendet, wenn mcache die mspan-Struktur von mcentral anfordert. Infolgedessen können Anforderungen von anderen mcache-anfordernden mspan-Strukturen anderer Größen gleichzeitig bedient werden.

Durch die Ausrichtung (Pad) wird sichergestellt, dass die zentralen Strukturen durch die Anzahl der Bytes, die dem CacheLineSize Wert entsprechen, CacheLineSize sind. Infolgedessen verfügt jede mcentral.lock über eine eigene Cache-Zeile, wodurch die Probleme vermieden werden, die mit einer falschen Speicherfreigabe verbunden sind.

Was passiert, wenn die mcentral-Liste leer ist? Dann empfängt mcentral eine Folge von Seiten von mheap, um Speicherfragmente der erforderlichen Größenklasse zuzuweisen.

  • free[_MaxMHeapList]mSpanList ist ein Array von spanList. Die mspan-Struktur in jeder spanList besteht aus 1 ~ 127 (_MaxMHeapList - 1) Seiten. Zum Beispiel ist free [3] eine verknüpfte Liste von mspan-Strukturen mit 3 Seiten. Das Wort "frei" zeigt in diesem Fall an, dass es sich um eine leere Liste handelt, in der kein Speicher zugeordnet ist. Eine Liste kann im Gegensatz zu einer leeren Liste eine Liste sein, in der Speicher zugewiesen ist (belegt).
  • freelarge mSpanList ist eine Liste der freien mspan-Strukturen. Die Anzahl der Seiten pro Element (dh mspan) beträgt mehr als 127. Zur Unterstützung dieser Liste wird die mtreap-Datenstruktur verwendet. Die Liste der ausgelasteten mspan-Strukturen wird als Busylarge bezeichnet.

Objekte, die größer als 32 KB sind, werden als große Objekte betrachtet. Der Speicher für sie wird direkt von mheap zugewiesen. Anforderungen zum Zuweisen von Speicher für solche Objekte werden unter Verwendung einer Sperre ausgeführt, so dass zu einem bestimmten Zeitpunkt eine ähnliche Anforderung von nur einem logischen Prozessor verarbeitet werden kann.

Der Prozess des Zuweisens von Speicher für Objekte


  • Wenn die Größe des Objekts 32 KB überschreitet, wird es als groß angesehen. Der Speicher dafür wird direkt von mheap zugewiesen.
  • Wenn die Größe des Objekts weniger als 16 KB beträgt, wird der als winziger Allokator bezeichnete mcache-Mechanismus verwendet.
  • Wenn die Größe des Objekts im Bereich von 16 bis 32 KB liegt, stellt sich heraus, welche Größenklasse (sizeClass) verwendet werden soll, und ein geeigneter Block wird in mcache zugewiesen.
  • Wenn in der sizeClass, die mcache entspricht, keine Blöcke verfügbar sind, wird mcentral aufgerufen.
  • Wenn mcentral keine freien Blöcke hat, rufen sie mheap auf und suchen nach dem am besten geeigneten mspan. Wenn sich herausstellt, dass die von der Anwendung benötigte Speichergröße größer ist als zugeordnet werden kann, wird die angeforderte Speichergröße verarbeitet, sodass so viele Seiten zurückgegeben werden können, wie vom Programm benötigt werden, nachdem eine neue mspan-Struktur gebildet wurde.
  • Wenn der virtuelle Speicher der Anwendung immer noch nicht ausreicht, wird für einen neuen Satz von Seiten auf das Betriebssystem zugegriffen (mindestens 1 MB Speicher wird angefordert).

Tatsächlich fordert Go auf Betriebssystemebene die Zuweisung noch größerer Speicherelemente, die als Arenen bezeichnet werden. Durch die gleichzeitige Zuweisung großer Speicherfragmente können Sie einen Kompromiss zwischen der der Anwendung zugewiesenen Speichermenge und dem kostspieligen Zugriff auf das Betriebssystem in Bezug auf die Leistung finden.

Der auf dem Heap angeforderte Speicher wird von der Arena zugewiesen. Betrachten Sie diesen Mechanismus.

Virtueller Speicher gehen


Sehen Sie sich die Speichernutzung mit einem einfachen Programm an, das in Go geschrieben wurde:

 func main() {   for {} } 


Programmprozessinformationen

Der virtuelle Adressraum selbst eines solchen einfachen Programms beträgt ungefähr 100 MB, während der RSS-Index nur 696 KB beträgt. Versuchen wir zunächst, den Grund für diese Diskrepanz herauszufinden.


Karten- und Smap-Informationen

Hier sehen Sie die Speicherbereiche, deren Größe ungefähr 2 MB, 64 MB, 32 MB entspricht. Was ist das für eine Erinnerung?

▍Arena


Es stellt sich heraus, dass der virtuelle Speicher in Go aus einer Reihe von Arenen besteht. Die anfängliche Speichergröße für den Heap entspricht einer Arena, dh - 64 MB (dies ist relevant für Go 1.11.5).


Aktuelle Arena-Größe in verschiedenen Systemen

Infolgedessen wird der für die aktuellen Anforderungen des Programms benötigte Speicher in kleinen Teilen zugewiesen. Dieser Prozess beginnt mit einer 64-MB-Arena.

Diese numerischen Indikatoren, über die wir hier sprechen, sollten nicht für einige absolute und unveränderte Werte herangezogen werden. Sie können sich ändern. Früher hat Go beispielsweise einen kontinuierlichen virtuellen Speicherplatz im Voraus reserviert. Auf 64-Bit-Systemen betrug die Arena-Größe 512 GB (es ist interessant zu überlegen, was passiert, wenn der reale Speicherbedarf so groß ist, dass die entsprechende Anforderung von mmap abgelehnt wird?).

Tatsächlich nennen wir eine Reihe von Arenen eine Reihe. In Go werden Arenen als Speicherfragmente wahrgenommen, die in Blöcke mit einer Größe von 8192 Bytes (8 KB) unterteilt sind.


Eine 64 MB Arena

Go hat noch ein paar verschiedene Arten von Blöcken - Span und Bitmap. Der Speicher für sie wird außerhalb des Heaps zugewiesen, sie speichern Arena-Metadaten. Sie werden hauptsächlich in der Müllabfuhr verwendet.
Hier finden Sie eine allgemeine Übersicht über die Funktionsweise der Speicherzuweisungsmechanismen in Go.


Allgemeiner Überblick über die Speicherzuweisungsmechanismen in Go

Zusammenfassung


Im Allgemeinen kann angemerkt werden, dass wir in diesem Material die Subsysteme für die Arbeit mit Go-Speicher sehr allgemein beschrieben haben. Die Hauptidee des Speichersubsystems in Go besteht darin, Speicher mithilfe verschiedener Strukturen und Caches auf verschiedenen Ebenen zuzuweisen. Dies berücksichtigt die Größe der Objekte, denen Speicher zugewiesen ist.

Die Darstellung eines einzelnen Blocks kontinuierlicher Speicheradressen, die vom Betriebssystem in Form einer mehrstufigen Struktur empfangen werden, erhöht die Effizienz des Speicherzuweisungsmechanismus aufgrund der Tatsache, dass dieser Ansatz das Blockieren vermeidet. Die Zuweisung von Ressourcen unter Berücksichtigung der Größe der Objekte, die im Speicher gespeichert werden müssen, verringert die Fragmentierung und ermöglicht es Ihnen, nach dem Freigeben des Speichers die Speicherbereinigung zu beschleunigen.

Liebe Leser! Haben Sie Probleme mit Speicherfehlern in in Go geschriebenen Programmen festgestellt?

Source: https://habr.com/ru/post/de442648/


All Articles