?
CStoll schrieb:
(Bei Sprachen mit Garbage Collector sieht die Sache möglicherweise wieder anders aus - in .NET werden die Lücken geschlossen, indem die Objekte im Speicher herumgeschoben werden, da reicht dann tatsächlich eine einfache Zeiger-Addition um ein Objekt anzulegen. Umgekehrt hast du dort etwas höheren Aufwand beim Zerstören der Objekte)
Ganz so einfach ist es mit GCs nicht. Eigentlich ist es sogar ziemlich kompliziert.
Es ist richtig, das bei Sprachen mit kompaktierendem GC die Erstallokation eines Objektes sehr fix ist (etwas mehr als eine Addition ist es schon, aber abgesehen vom Locking nicht allzu viel). Das ist allerdings nicht das Ende der Geschichte. Damit die Erstallokation so schnell geht, muss der Garbage-Collector den betreffenden Bereich des Heaps freihalten. Ich erkläre es mal anhand der Hotspot-JVM; das und ähnliche Verfahren sind allgemein üblich.
Hotspot verwendet einen Generationsheap. Der Aufbau ist etwa so:
| Young Generation | Tenured |
+------+------------+------------+------------------------------------+
| Eden | Survivor 1 | Survivor 2 | |
+------+------------+------------+------------------------------------+
Neue Objekte werden in Eden angelegt, Survivor 1 oder 2 beinhaltet relativ junge Objekte, die ein paar GCs überlebt haben, der jeweils andere Survivor ist leer, und Tenured ist der Teil des Heaps, in dem alte Objekte leben. Hier gibt es echte Probleme mit Fragmentierung, weswegen von Zeit zu Zeit (aber eher selten) kompaktiert wird. Die Kompaktierung ist sehr aufwändig.
Wenn Eden vollläuft, wird der Garbage Collector auf die junge Generation losgelassen. Alle Objekte in Eden und dem belegten Survivor werden geprüft und die überlebenden in den leeren Survivor-Space umgelegt. Solche, die inzwischen alt genug sind, werden nach Tenured verschoben. Der Vorteil dieses Verfahrens ist, dass man sich im jungen Heap keine Gedanken um Fragmentierung machen muss.
Der Nachteil ist, dass die Threads sich dauernd um den Heap prügeln. Der Garbage-Collector kann die Objekte nicht verschieben, wenn sie von anderen Threads gerade benutzt werden, also muss die virtuelle Maschine sehr vorsichtig sein, was das Scheduling betrifft. Ferner ist ein Teil des GC-Laufs ein Stop-The-World-Event, d.h. während eines Teils des GC-Laufs (hauptsächlich der Kopiererei) darf kein anderer Thread arbeiten, und wenn er damit durch ist, darf kein Thread gecachte Adressen benutzen. Das alles erfordert einen gewissen Aufwand, und in nebenläufigen Szenarien kann es unter bestimmten Umständen tödlich wirken.
Das Verfahren funktioniert solange hinreichend schnell, wie bei einem GC-Lauf fast alle Objekte tot sind. Konkret bedeutet das: Wenn man im Zusammenhang mit einem GC hauptsächlich kurzlebige Objekte auf den Heap legt (größtenteils solche, die man in C auf den Stack gelegt hätte), hat man mit einem GC verkraftbare Performanceeinbußen bei der Allokation. Das ist auch das Hauptanliegen der ganzen Geschichte. Wenn ein Objekt etwas länger lebt, verursacht es durch die Kopiererei einiges an Mehraufwand, und wenn es langlebig genug ist, um in Tenured zu landen, kommt zusätzlich zu diesem Aufwand bei nachfolgenden GC-Läufen in Tenured eine Erschwerung der Kompaktierung hinzu - besonders kräftig am Ende seines Lebenszyklus, wenn es den Heap fragmentiert, obwohl das verhältnismäßig weniger ins Gewicht fällt, je länger der Lebenszyklus andauert.
Was Stack-Allokation angeht, bleibt noch anzumerken, dass, sofern die Stackvariable, um die es geht, nicht die einzige in der Funktion ist, ihre Allokation überhaupt nichts kostet, weil jeder Compiler der Welt den Stackzeiger bei Eintritt in die Funktion einmal für alle Variablen erhöhen wird. Speichermanager brauchen auch amortisiert mindestens einen Funktionsaufruf plus was immer die Strategie, die der Speichermanager verfolgt, benötigt. Ergo sind Speichermanager pauschal langsamer als der Stack, auch wenn es mitunter gute Gründe für ihre Verwendung gibt.
Die Argumentation "Jaaa, aber wenn man den Kram, den man auf den Stack packt, nachher doch in dynamischem Speicher braucht, dann ist ein Speichermanager schneller" kann ja wohl nicht ernst gemeint sein.
Gleichwohl: cookys Argumentation ist nicht vollständig. Man kann sich hier wohl auf eine feste Obergröße festlegen, wenn man das tut, muss man aber aktiv darauf achten, diese auch einzuhalten. Nachträglich sei also snprintf erwähnt:
char pathcom[1024]; /* Sollte ausreichen */
if(snprintf(pathcom, 1024, "ls -l %s > ../lists/indir.txt", argv[1]) < 1024)) {
system(pathcom);
} else {
/* Eingabe zu lang */
}
Abhängig davon, was er nachher damit machen will, wäre es aber womöglich sinnvoller, mit opendir() etc. zu arbeiten.