Christian Seiler: GC in 5.3

Beitrag lesen

Hallo,

Wie sieht es jetzt genau mit dem GC aus. Gibt dieser Speicher (endlich) auch an das OS wieder ab, während Scripte laufen und Variablen gelöscht werden? (Mein Interesse gilt da CLI-Anwendungen.)

Den GC betrifft das nicht, bei dem geht's nur um zirkuläre Referenzen. PHP gibt ja auch bereits in 5.2 schon Speicher an das OS zurück, wenn bestimmte Bedingungen erfüllt sind - und an der Speicherverwaltung selbst hat sich nichts wesentliches geändert.

Um zu verstehen, warum PHP bereits in 5.2 den Speicher nicht wirklich freigibt, eine kurze Einführung, wie in PHP die Speicherverwaltung funktioniert:

Sobald Speicher benötigt wird, ruft der C-Code von PHP die Funktion emalloc() auf. Die Funktion fordert dann vom Betriebssystem einen Block Speicher an, der der Anwendung zur Verfügung stehen soll (deswegen steigt der Speicherverbrauch auch in Sprüngen an) - und fügt den Block zu der internen Liste mit zur Verfügung stehenden Blöcke hinzu. Aus diesem Block wird dann ein Teil reserviert und die Adresse davon zurückgegeben. Beispiel (Zahlen für die Blockgrößen aus den Fingern gesogen):

0. Zustand der Speicherverwaltung und des Speichers:

+------------+
 | Blockliste |
 +------------+
 | (leer)     |
 +------------+

1. Aufruf von emalloc:

char *foo = emalloc (20);

1a. Anfordern eines größeren Blocks vom Betriebssystem

[Notation: XXX reserviert, sonst frei]

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|                                        |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+
 +------------+

1b. Reservieren des Speicherbereichs im Block und Zurückgeben des Zeigers.

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|XXXXX                                   |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+            ^
 +------------+                  |
                                 |
  foo ---------------------------+

2. Weiterer Aufruf von emalloc:

char *bar = emalloc (28);

2a. Reservieren des Speicherbereichs im Block und Zurückgeben des Zeigers.

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|XXXXXXXXXXXX                            |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+            ^    ^
 +------------+                  |    |
                                 |    |
  foo ---------------------------+    |
                                      |
  bar --------------------------------+

3. Noch ein Aufruf von emalloc:

char *baz = emalloc (200);

3a. Feststellen, dass der bisherige Speicherbereich zu klein ist, also noch ein Block vom OS anfordern:

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|XXXXXXXXXXXX                            |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+            ^    ^
 | Block 2  --|---+              |    |
 +------------+   |              |    |
                  |              |    |
  foo ---------------------------+    |
                  |                   |
  bar --------------------------------+
                  |             +------------- ... ----------------------+
                  +------------>|              ...                       |
                                +------------- ... ----------------------+

2a. Reservieren des Speicherbereichs im neuen Block und Zurückgeben des Zeigers.

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|XXXXXXXXXXXX                            |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+            ^    ^
 | Block 2  --|---+              |    |
 +------------+   |              |    |
                  |              |    |
  foo ---------------------------+    |
                  |                   |
  bar --------------------------------+
                  |             +------------- ... ----------------------+
                  +------------>|XXXXXXXXXXXXX ... XXXXXXXX              |
                                +------------- ... ----------------------+
                                 ^
                                 |
  baz ---------------------------+

Was passiert nun, wenn im Script eine Variable nicht mehr benötigt wird? Der Speicherbereich der Variable wird als frei gekennzeichnet. In unserem Beispiel:

efree (foo);
foo = NULL;

+------------+                 +----------------------------------------+
 | Blockliste |     +---------->|     XXXXXXX                            |
 +------------+     |           +----------------------------------------+
 | Block 1  --|-----+                 ^
 | Block 2  --|---+                   |
 +------------+   |                   |
                  |                   |
  foo             |                   |
                  |                   |
  bar --------------------------------+
                  |             +------------- ... ----------------------+
                  +------------>|XXXXXXXXXXXXX ... XXXXXXXX              |
                                +------------- ... ----------------------+
                                 ^
                                 |
  baz ---------------------------+

Das Problem ist nun: Der Speicher des ersten Blocks kann nicht von PHP wieder an das Betriebssystem zurückgegeben werden, da dort noch die Variable bar lebt. Und das war hier nur im einfachen Beispiel - in PHP gibt es z.B. pro globaler Variable etliche Allozierungsaufrufe. Man hat also am Ende lauter Blöcke, die PHP hat, die teilweise belegt sind -> der Speicherverbrauch sinkt nicht mehr. Er sinkt nur dann, wenn Blöcke am Stück zurückgegeben werden können was eigentlich nur dann passiert, wenn man z.B. PHP als Apache-Modul betreibt können am Ende des Requests alle Blöcke, die während des Requests alloziert wurden, wieder freigegeben werden.

Nun könntest Du fragen, warum PHP überhaupt Blöcke alloziert die größer sind als der angeforderte Bereich und nicht einfach die immer passend macht. Die Antwort darauf ist, dass es sehr langsam ist, vom Betriebssystem einen neuen Block anzufordern (fast unabhängig von der Größe des Blocks), es aber sehr schnell ist, intern Teile von Blöcken als belegt oder nicht belegt zu markieren. Außerdem müssen die Blöcke vom Betriebssystem "aligned" sein, d.h. können ihre Startadresse nur in Vielfachen der Page Size oder einer ähnlichen Größe haben - und bei 4 KB Page Size würdest Du um nur 100 Bytes zu allozieren ganze 3996 Bytes komplett verschwenden. Daher ist die gewählte Variante sicherlich besser als die vorige.

Allerdings: Der nichtverwendete Speicher in den Blöcken steht wieder für neue Variablen in PHP zur Verfügung, d.h. der Speicherverbrauch eines PHP-Scripts geht zwar in der Regel nicht mehr nach unten - aber wenn es gut geht steigt er auch nicht mehr weiter.

Viele Grüße,
Christian

--
Mein "Weblog" [RSS]
Using XSLT to create JSON output (Saxon-B 9.0 for Java)
»I don't believe you can call yourself a web developer until you've built an app that uses hyperlinks for deletion and have all your data deleted by a search bot.«
            -- Kommentar bei TDWTF