-
Die
Erfindung betrifft ein Verfahren zur Verwaltung von Speicher von
digitalen Recheneinrichtung.
-
Moderne
Recheneinrichtungen ermöglichen auf
Grund des großen
vorhandenen Speichers und der enormen Rechenleistungen die Verwendung komplexer
Programme. Durch diese Programme laufen auf der Recheneinrichtung
Prozesse ab, innerhalb derer mehrere sogenannte Threads gleichzeitig abgearbeitet
werden. Da viele dieser Threads zeitlich nicht unmittelbar aufeinander
abgestimmt sind, kann es vorkommen, dass mehrere Threads gleichzeitig auf
die Speicherverwaltung und damit potentiell auf einen bestimmten
Block des verfügbaren
Speichers zuzugreifen versuchen. Ein solcher gleichzeitiger Zugriff
kann zu einer Systeminstabilität
führen.
Durch einen Eingriff des Betriebssystems kann ein solcher gleichzeitiger
Zugriff auf einen bestimmten Speicherblock jedoch verhindert werden.
Das Verhindern des Zugriffs auf einen bereits im Zugriff befindlichen Speicherblock
durch einen weiteren Thread ist in der
DE 679 15 532 T2 beschrieben.
Dabei wird ein gleichzeitiger Zugriff nur dann verhindert, wenn
der gleichzeitige Zugriff denselben Speicherblock betrifft.
-
In
gängigen
Speicherverwaltungen werden beispielsweise häufig sogenannte doppelt verkettete Listen
zur Verwaltung des gesamten Speichervolumens in einzelnen Speicherobjekten
verwendet. Bei diesen doppelt verketteten Listen erfolgt ein Zugriff auf
ein bestimmtes Speicherobjekt in mehreren Schritten. Es ist daher
erforderlich, mit dem ersten Zugriff auf ein solches Speicherobjekt
die übrigen Threads
zu sperren, so dass ein gleichzeitiger Zugriff durch einen weiteren
Thread nicht möglich
ist, bevor die einzelnen Schritte des ersten Zugriffs abgearbeitet
sind. Diese Zugriffssperrung erfolgt unter Zuhilfenahme des Betriebssystems
durch eine sogenannte Mutex-Routine. Durch das Einbinden des Betriebssystems
und das Ausführen
der Mutex-Routine geht jedoch kostbare Rechenzeit verloren. In dieser
Zeit sind die übrigen
Threads blockiert, indem sie durch Mutex-basiertes Locking vom Betriebssystem
vorübergehend
von der Ausführung
abgehalten werden.
-
Es
ist die Aufgabe der Erfindung, ein Verfahren zur Verwaltung von
Speicher einer digitalen Recheneinheit zu schaffen, bei dem in einer
Multithread-Umgebung der gleichzeitige Zugriff durch nebenläufige Threads
auf einen bestimmten Speicherblock unmöglich ist, das aber gleichzeitig
kurze Speicherzugriffszeiten erlaubt.
-
Die
Aufgabe wird durch das erfindungsgemäße Verfahren nach Anspruch
1 gelöst.
-
Anstelle
von doppelt verketteten Listen wird erfindungsgemäß eine Stapelverwaltung
für den
verfügbaren
Speicher verwendet. Hierzu wird in dem verfügbaren Speicherbereich zunächst zumindest ein
solcher Stapel angelegt. Die Aufnahme und Rückgabe eines Speicherobjekts
durch einen Thread erfolgt dann durch jeweils eine atomare Operation. Durch
Verwenden einer solchen atomaren Operation für den Speicherzugriff zusammen
mit einer Stapelorganisation des Speichers, der lediglich einen
Zugriff auf das letzte Objekt des Stapels ermöglicht, ist eine weitergehende
Blockierung der übrigen
Threads nicht erforderlich. Dabei wird durch die atomare Operation
bereits gewährleistet,
dass der Zugriff auf das Speicherobjekt in lediglich einem einzigen
Schritt erfolgt, wodurch ein Überlapp
mit parallel ablaufenden Schritten weiterer Threads nicht auftreten
kann.
-
In
den Unteransprüchen
sind vorteilhafte Weiterbildungen des erfindungsgemäßen Verfahrens beansprucht.
-
Ein
bevorzugtes Ausführungsbeispiel
ist in den Figuren dargestellt und wird in der nachfolgenden Beschreibung
näher erläutert. Es
zeigen:
-
1 eine
schematische Darstellung einer bekannten Speicherverwaltung mit
doppel verketteten Listen;
-
2 eine
Speicherverwaltung mittels Stapeln und atomarer Aufnahme- und Rückgabefunktionen
und
-
3 eine
schematische Darstellung über den
Verfahrensablauf der erfindungsgemäßen Speicherverwaltung.
-
Bei
sogenannten doppelt verketteten Listen wird der Speicher in mehrere
Speicherobjekte 1, 2, 3 und 4 eingeteilt,
die in 1 schematisch dargestellt sind. Innerhalb eines
jeden solchen Speicherobjekts 1 bis 4 sind ein
erstes Feld 1a und ein zweites Feld 1b angelegt.
Dabei verweist das erste Feld 1a des ersten Speicherobjekts 1 auf
die Position des zweiten Speicherobjekts 2. Ebenso verweist
das erste Feld 2a des zweiten Speicherobjekts 2 auf
die Position des dritten Speicherobjekts 3 u.s.w.. Um die
Aufnahme eines beliebigen mittleren Blocks zu ermöglichen, ist
nicht nur in Vorwärtsrichtung
die Position des jeweils nächsten
Speicherobjekts angegeben, sondern es ist in dem zweiten Feld 2b, 3b und 4b der
Speicherobjekte 2, 3 und 4 die Position
des jeweils vorausgehenden Speicherobjekts 1, 2 und 3 angegeben.
Auf diese Weise ist es möglich,
einen zwischen zwei Speicherobjekten angeordnetes Speicherobjekt herauszunehmen
und gleichzeitig die Felder der benachbarten Speicherobjekte zu
aktualisieren.
-
Solche
doppelt verketteten Listen ermöglichen
zwar den individuellen Zugriff auf ein beliebiges Speicherobjekt,
weisen andererseits aber den Nachteil auf, dass in einer Multithread-Umgebung
der gleichzeitige Zugriff mehrere Threads auf ein Speicherobjekt
nur mit langsamen Operationen zu verhindern ist. Eine Möglichkeit
ist die in der Einleitung bereits beschriebene Verwaltung der Zugriffe über die Mutex-Funktion.
Das erste Speicherobjekt 1 in einer Liste ist durch einen
speziellen Zeiger 5 erreichbar und außerdem dadurch gekennzeichnet,
dass in dem zweiten Feld 1b anstelle der Position eines
vorausgehenden Speicherobjekts ein Nullvektor abgelegt ist. Dementsprechend
wird das Speicherobjekt 4 als letztes gekennzeichnet, indem
anstelle der Position eines weiteren Speicherobjekts in dem ersten
Feld 4a des Speicherobjekts 4 ein Nullvektor abgelegt
ist.
-
In
der 2 ist dagegen ein Beispiel einer erfindungsgemäßen Speicherverwaltung
dargestellt. Bei der erfindungsgemäßen Speicherverwaltung werden
zunächst
bei einer Initatialisierung vorzugsweise mehrere Stapel oder Stacks
angelegt. Diese Stacks sind eine spezielle Form einfach verketteter Listen.
In der 2 sind vier solcher Stacks dargestellt, die mit
den Bezugszeichen 6, 7, 8 und 9 bezeichnet
sind. Jeder dieser Stacks 6 bis 9 umfasst mehrere
Speicherobjekte unterschiedlicher Größe. So können in dem ersten Stack 6 Objekte
bis zu einer Größe von 16
Bytes, in dem zweiten Stack 7 Objekte bis zu einer Größe von 32
Bytes, in dem dritten Stack 8 Objekte bis zu einer Größe von 64
Bytes und schließlich
in dem vierten Stark 9 Objekte bis zu einer Größe von 128
Bytes gespeichert werden. Bei Auftreten größerer zu speichernder Elemente
können
auch Stacks mit größeren Speicherobjekten
angelegt werden, wobei vorzugsweise jeweils zum nächsten Stark die
Größe der einzelnen
Speicherobjekte verdoppelt wird. Die Aufteilung eines solchen Stacks
in einzelne Speicherobjekte 10.i ist für den vierten Stark 9 detailliert
dargestellt. Der vierte Stark 9 besteht aus einer Reihe
einfach miteinander verketteten Speicherobjekten 10.1, 10.2, 10.3 ...
bis 10.k. Das letzte Speicherobjekt 10.k des vierten
Stacks 9 ist in der 2 leicht
abgesetzt dargestellt. Ein Zugriff auf die einzelnen Speicherobjekte
ist auf allen Stapeln 6 bis 9 jeweils nur für das unterste
Speicherobjekt der Stapel 6 bis 9 möglich, beispielsweise
auf Stapel 9 nur für das
Speicherobjekt 10.k.
-
In
der 2 kann folglich bei einer Anforderung von Speicher
z. B. das letzte Speicherobjekt 10.k des vierten Stapels 9 verwendet
werden. Wird das Speicherobjekt 10.k wieder frei, da es
durch einen Thread nicht länger
benötigt
wird, so wird es entsprechend am Ende des vierten Stacks 9 wieder
zurückgegeben.
-
In
der 2 ist dies schematisch durch eine Anzahl unterschiedlicher
Threads 11, durch die jeweils eine Speicheranforderung
gegeben ist, dargestellt. Bei dem konkreten Ausführungsbeispiel fordert beispielsweise
ein Prozess in mehreren Threads 12, 13 und 14 Speichervolumen
derselben Größe an. Die Größe des angeforderten
Speichers ergibt sich aus den zu speichernden Daten. In dem dargestellten Ausführungsbeispiel
wird der vierte Stack 9 ausgewählt, sobald ein Speicherbedarf
von mehr als 64 Bytes bis zu einer Maximalgröße von 128 Bytes vorhanden
ist. Wird nun durch den ersten Thread 12 ein Speichervolumen
von beispielsweise 75 Bytes benötigt,
so wird zunächst
derjenige der Stacks 6 bis 9 ausgewählt, der
ein freies Speicherobjekt passender Größe enthält. In dem dargestellten Ausführungsbeispiel
ist dies der vierte Stack 9. Hier werden Speicherobjekte 10.i mit
einer Größe von 128
Bytes zur Verfügung
gestellt. Da das Speicherobjekt 10.k das letzte Speicherobjekt
in dem vierten Stack 9 ist, wird auf Grund der Speicheranfrage
des ersten Threads 12 eine sogenannte "Pop"-Operation
abgearbeitet und damit das Speicherobjekt 10.k dem Thread 12 zur
Verfügung
gestellt.
-
Eine
solche Pop-Routine ist atomar bzw. unteilbar, d. h. das Speicherobjekt 10.k wird
für den Thread 12 in
einem einzigen Verarbeitungsschritt von dem vierten Stack 9 abgenommen.
Durch diese atomare bzw. unteilbare Operation, mit der das Speicherobjekt 10.k dem
Thread 12 zugeordnet wird, wird verhindert, dass ein anderer
Thread, beispielsweise der Thread 13 auf dasselbe Speicherobjekt 10.k gleichzeitig
zugreifen kann. D. h., sobald ein neuer Verarbeitungsschritt durch
das System durchgeführt werden
kann, ist die Verarbeitung hinsichtlich des Speicherobjekts 10.k abgeschlossen
und das 10.kte Speicherobjekt nicht länger Bestandteil des vierten Stacks 9.
Bei einer weiteren Speicheranforderung durch den Thread 13 ist
daher das dann letzte Speicherobjekt des vierten Stacks 9 das
Speicherobjekt 10.k-1. Auch hier wird wiederum zur Übergabe
des Speicherobjekts 10.k-1 an den Thread 13 eine
atomare Pop-Operation durchgeführt.
-
Solche
atomaren Operationen setzen hardwareseitig entsprechende Unterstützung voraus
und können
nicht direkt in normalen Programmiersprachen formuliert werden,
sondern erfordern den Einsatz von Maschinensprache. Erfindungsgemäß werden
diese hardwareseitig implementierten, üblicherweise jedoch nicht zur
Speicherverwaltung verwendeten sogenannten Lock-Free-Pop-Aufrufe bzw. Lock-Free-Push-Aufrufe
zur Verwaltung von Speicher eingesetzt. Hierzu wird beispielsweise
anstelle der doppelt verketteten Listen, wie sie in der 1 schematisch
dargestellt wurden, eine einfach verkettete Liste verwendet, bei
der lediglich an einem Ende der angelegten Stacks Speicherobjekte
abgerufen bzw. zurückgegeben
werden können.
-
In
der 2 ist es weiterhin für eine Anzahl von Threads 15 dargestellt,
wie nach einem Delete-Aufruf eines Threads das jeweilige freiwerdende Speicherobjekt
wieder zurück
zu dem passenden Stack gegeben wird. In jedem der Speicherobjekte 10.i ist
ein Header 10.ihead vorhanden,
wie es für
das Speicherobjekt 10.k in der 2 gezeigt
ist, in dem die Zuordnung zu einem bestimmten Stack kodiert ist.
So ist beispielsweise in dem Header 10.khead die Zuordnung
zu dem vierten Stack 9 enthalten. Wird nun durch einen
Thread 16, dem auf Grund einer entsprechenden Lock-Free-Pop-Operation
das Speicherobjekt 10.k zugeordnet wurde, eine Delete-Funktion
aufgerufen, so wird durch eine entsprechende, ebenfalls atomare
Lock-Free-Push-Operation das Speicherobjekt 10.k zurückgegeben.
Das Speicherobjekt 10.k wird dabei an das letzte zu dem vierten
Stack 9 gehörende
Speicherelement 10.k-1 angehängt. Damit wird abhängig von
der Reihenfolge in der unterschiedliche Threads 16, 17, 18 die
Speicherobjekte 10.i zurückgeben, die Reihenfolge der Speicherobjekte 10.i in
dem vierten Stack 9 geändert.
-
Entscheidend
ist es, dass diese sogenannten Lock-Free-Pop- und Lock-Free-Push-Aufrufe atomar sind
und daher extrem schnell abgearbeitet werden können. Der Geschwindigkeitsvorteil
beruht dabei entscheidend darauf, dass die Verwendung einer Betriebssystemoperation,
beispielsweise Mutex, nicht erforderlich ist, um weitere Threads
von einem gleichzeitigen Zugriff auf ein bestimmtes Speicherobjekt
auszuschließen.
Ein solcher Ausschluss für
den gleichzeitigen Zugriff durch weitere Threads ist auf Grund der
atomaren Eigenschaft der Pop- bzw. Push-Aufrufe
nicht erforderlich. Insbesondere muss das Betriebssystem bei einem
tatsächlich
gleichzeitigen Zugriff auf die Speicherverwaltung (sog. Contention-Fall)
keinen Threadwechsel durchführen,
der im Vergleich zur Speicheroperation selbst ungleich mehr Rechenzeit
erfordert.
-
Bei
einer solchen Verwaltung von Speicher in Stacks und Zugriffen mittels
Lock-Free-Pop- und Lock-Free-Push-Aufrufen ist es unvermeidbar, dass ein
Teil des vorhandenen Speichervolumens verschenkt wird. Diese Verschwendung
entsteht durch die nicht ideal angepasste Größe der einzelnen Stacks bzw.
deren Speicherobjekte. Ist eine bestimmte Größenstruktur der zu speichernden
Daten bekannt, so kann jedoch die Verteilung der Speicherobjektgrößen der
einzelnen Stacks 6 bis 9 hieran angepasst werden.
-
Gemäß einer
besonders bevorzugten Form der erfindungsgemäßen Speicherverwaltung ist
es vorgesehen, dass zu Beginn eines Prozesses, beispielsweise nach
einem Programmstart, die zum Prozessablauf benötigten Stacks 6 bis 9 lediglich
initialisiert werden, zu diesem Zeitpunkt aber noch keine Speicherobjekte 10.i enthalten.
Wird nun ein Speicherobjekt einer bestimmten Größe zum ersten Mal benötigt, beispielsweise
für ein
zu speicherndes Element der Größe 50 Bytes
ein Speicherobjekt des dritten Stacks 8, so wird diese
erste Speicheranforderung über
die langsamere Systemspeicherverwaltung abgearbeitet und das Speicherobjekt
von dort zur Verfügung
gestellt. Der gleichzeitige Zugriff wird in dem erläuterten
Beispiel doppelt verketteter Listen als Systemspeicherverwaltung
durch eine langsame Mutex Opertation verhindert. Das auf diese Art
und Weise einem ersten Thread zur Verfügung gestellte Speicherobjekt
wird jedoch nach einem Delete-Aufruf nicht
wieder über
die langsamere Systemspeicherverwaltung zurückgegeben, sondern es wird
auf einen entsprechenden Stack, im beschriebenen Ausführungsbeispiel
den dritten Stack 8 durch eine Lock-Free-Push-Operation abgelegt. Für den nächsten Aufruf
eines Speicherobjekts dieser Größe kann daher
auf dieses Speicherobjekt mit einer sehr schnellen Lock-Free-Pop-Operation zugegriffen
werden.
-
Diese
Vorgehensweise hat den Vorteil, dass nicht pauschal zu Beginn eines
Prozesses bereits eine festgelegte Anzahl von Speicherobjekten den einzelnen
Stacks 6, 7, 8 und 9 zugeordnet
werden muss. Vielmehr kann dynamisch der Speicherbedarf an den laufenden
Prozess bzw. dessen Threads angepasst werden. Läuft beispielsweise ein Prozess
im Hintergrund mit wenigen nebenläufigen Threads ab und hat nur
geringen Bedarf an Speicherobjekten, so werden bei einer solchen
Vorgehensweise entscheidend Resourcen gespart.
-
Das
Verfahren ist schematisch noch einmal in 3 dargestellt.
Zunächst
wird in Schritt 19 ein Programm gestartet und somit ein
Prozess auf beispielsweise einem Computer generiert. Mit dem Beginn
des Prozesses werden mehrere Stacks 6 bis 9 initialisiert.
Die Initialisierung der Stacks 6–9 ist in Schritt 20 dargestellt.
In dem in der 3 dargestellten Ausführungsbeispiel
des Verfahrensablaufs werden zunächst
nur einzelne Stacks 6–9 angelegt,
ohne diese jedoch mit einer bestimmten vordefinierten Anzahl von
Speicherobjekten zu füllen.
Bei einer im Verfahrensschritt 21 auftretenden Speicheranforderung durch
einen Thread wird zunächst
auf Grund der durch den Thread festgelegten Objektgröße ein entsprechender
Stack selektiert.
-
Wird
beispielsweise ein 20 Bytes großes Speicherobjekt
benötigt,
so wird in der Stack-Auswahl der 2 der zweite
Stack 7 gewählt.
Anschließend
wird in Schritt 23 die atomare Pop-Operation eine Abfrage
durchgeführt.
Bestandteil dieser unteilbaren Operation ist eine Abfrage 26,
ob in dem zweiten Stack 7 ein Speicherobjekt verfügbar ist.
Für den Fall,
dass der Stack 7 mit einer Größe von 32 Bytes pro Speicherobjekt
lediglich initialisiert ist, jedoch noch kein verfügbares Speicherobjekt
enthält,
wird ein Nullvektor ("NULL") zurückgegeben
und über
einen Systemaufruf in Schritt 24 über die langsamere Systemspeicherverwaltung
ein Speicherobjekt der Größe 32 Bytes
zur Verfügung
gestellt. Die Größe des zur
Verfügung
gestellten Speicherobjekts wird dabei jedoch nicht unmittelbar durch
den Thread in Schritt 21 festgelegt, sondern über die
Auswahl einer bestimmten Objektgröße in Schritt 22 unter
Berücksichtigung
der initialisierten Stapel.
-
In
dem beschriebenen Ausführungsbeispiel wird
folglich die Speicheranfrage so geändert, dass ein Speicherobjekt
mit der Größe 32 Bytes
angefordert wird. Um einen gleichzeitigen Zugriff auf dieses Speicherobjekt
während
der Aufnahme durch den Thread zu verhindern, würde in dem Beispiel der Systemspeicherverwaltung
mittels doppelt verketteter Listen über das Betriebssystem eine
Mutex-Operation
gestartet.
-
Handelt
es sich bei dem benötigten
Speicherobjekt dagegen um ein bereits im Laufe des Prozesses zurückgegebenes
Speicherobjekt, so liegt dieses bereits in dem zweiten Stack 7 vor.
Die Abfrage in Schritt 26 wäre daher mit "Ja" zu beantworten und
es wird unmittelbar ein Speicherobjekt geliefert. Der Vollständigkeit
halber ist im weiteren Verlauf des Verfahrens sowohl für einen
mittels Lock-Free-Pop-Aufruf
als auch über
die Systemspeicherverwaltung zur Verfügung gestellten Speicherobjekt
die Rückgabe
desselben auf Grund eines Delete-Aufrufs dargestellt. Der Ablauf
in Folge eines Delete-Aufrufs eines Threads ist für beide
Situationen identisch. D. h. hier wird in keiner Weise Rücksicht darauf
genommen, auf welchem Weg das Speicherobjekt zur Verfügung gestellt
wurde. In der 3 ist dies schematisch durch
die beiden parallelen Wege dargestellt, wobei auf der rechten Seite
gestrichene Bezugszeichen verwendet werden.
-
Zunächst wird
durch einen Thread ein Delete-Aufruf gestartet. Der entsprechende
Speicherobjekt wird über
ein Auswerten der Informationen des Headers des Speicherobjekts
einem bestimmten Stack zugeordnet. In dem beschriebenen Ausführungsbeispiel
wird folglich das Speicherobjekt mit der Größe 32 Bytes dem zweiten
Stack 7 zugeordnet. Die Rückgabe des Speicherobjekts
an den zweiten Stack 7 erfolgt in beiden Fällen über eine Lock-Free-Push-Operation 29 bzw. 29'. Mit dem letzten
Verfahrensschritt 30 ist angedeutet, dass das so zurückgegebene
Speicherobjekt des zweiten Stacks 7 somit für einen
nächsten
Aufruf zur Verfügung steht.
Dieser nächste
Aufruf kann dann, wie es bereits erläutert wurde, durch eine Lock-Free-Pop-Operation
einem Thread zur Verfügung
gestellt werden.
-
Wie
es bereits beschrieben wurde, kann bei der Initalisierung der Stacks 6 bis 9 eine
Verringerung der Speicherverschwendung erreicht werden, indem Häufigkeitsverteilungen
für angeforderte
Objektgrößen erstellt
werden. Dies kann auch während
des Abarbeitens verschiedener Prozesse für die einzelnen Prozesse festgelegt
werden. Wird ein solcher Prozess mit seinen nebenläufigen Threads
ein weiteres Mal gestartet, so wird auf die zuvor ermittelte Häufigkeitsverteilung
aus dem vorangegangenen Prozessdurchlauf zurückgegriffen, um eine angepasste
Größenverteilung
der Stacks 6 bis 9 zu ermöglichen. Das System kann selbstlernend
ausgeführt
werden, d. h. dass mit jedem neuen Durchlauf die bereits gewonnenen
Erkenntnisse über
die Größenverteilungen des
Speicherbedarfs aktualisiert werden und bei jedem neuen Aufruf des
Prozesses die jeweils aktualisierten Daten verwendet werden.
-
Die
Erfindung ist nicht auf das dargestellte Ausführungsbeispiel beschränkt. Vielmehr
sind beliebige Kombinationen der einzelnen erläuternden Merkmale möglich.