Ist das nicht ein Gedächtnisaufwand?
Ja Nein Vielleicht?
Dies ist eine heikle Frage, da Sie sich den Adressierungsbereich des Speichers auf dem Computer und eine Software vorstellen müssen, die permanent den Speicherort auf eine Weise festhält, die nicht an den Stapel gebunden werden kann.
Stellen Sie sich beispielsweise einen Musik-Player vor, bei dem die Musikdatei auf Knopfdruck des Benutzers geladen und aus dem flüchtigen Speicher entfernt wird, wenn der Benutzer versucht, eine andere Musikdatei zu laden.
Wie verfolgen wir, wo die Audiodaten gespeichert sind? Wir brauchen eine Speicheradresse dazu. Das Programm muss nicht nur die Audiodaten im Speicher verfolgen, sondern auch, wo sie sich im Speicher befinden. Daher müssen wir eine Speicheradresse (dh einen Zeiger) behalten. Die Größe des für die Speicheradresse erforderlichen Speichers entspricht dem Adressierungsbereich des Geräts (Beispiel: 64-Bit-Zeiger für einen 64-Bit-Adressierungsbereich).
Es ist also eine Art "Ja", es erfordert Speicher, um eine Speicheradresse zu verfolgen, aber es ist nicht so, als könnten wir es für dynamisch zugewiesenen Speicher dieser Art vermeiden.
Wie wird das kompensiert?
Wenn Sie nur über die Größe eines Zeigers selbst sprechen, können Sie in einigen Fällen die Kosten vermeiden, indem Sie den Stapel verwenden. In diesem Fall können Compiler Anweisungen generieren, die die relative Speicheradresse effektiv fest codieren, wodurch die Kosten eines Zeigers vermieden werden. Dies macht Sie jedoch anfällig für Stapelüberläufe, wenn Sie dies für Zuweisungen mit großen variablen Größen tun, und es ist in der Regel unpraktisch (wenn nicht sogar unmöglich), dies für eine komplexe Reihe von Zweigen zu tun, die durch Benutzereingaben gesteuert werden (wie im Audiobeispiel) über).
Eine andere Möglichkeit besteht darin, zusammenhängendere Datenstrukturen zu verwenden. Beispielsweise kann eine Array-basierte Sequenz anstelle einer doppelt verknüpften Liste verwendet werden, für die zwei Zeiger pro Knoten erforderlich sind. Wir können auch eine Kreuzung dieser beiden Elemente wie eine nicht gerollte Liste verwenden, in der nur Zeiger zwischen jeder zusammenhängenden Gruppe von N Elementen gespeichert sind.
Werden Zeiger in zeitkritischen Anwendungen mit geringem Arbeitsspeicher verwendet?
Ja, sehr häufig, da viele leistungskritische Anwendungen in C oder C ++ geschrieben sind, die von der Zeigerverwendung dominiert werden (sie können sich hinter einem intelligenten Zeiger oder einem Container wie std::vector
oder befinden)std::string
, aber die zugrunde liegende Mechanik läuft auf einen Zeiger hinaus, der verwendet wird um die Adresse in einem dynamischen Speicherblock zu verfolgen).
Nun zurück zu dieser Frage:
Wie wird das kompensiert? (Zweiter Teil)
Zeiger sind in der Regel spottbillig, es sei denn, Sie speichern wie eine Million von ihnen (was auf einem 64-Bit-Computer immer noch mickrig 8 Megabyte ist).
* Beachten Sie, wie Ben darauf hinwies, dass "dürftige" 8 Megabyte immer noch die Größe des L3-Caches haben. Hier habe ich "dürftig" mehr im Sinne der gesamten DRAM-Nutzung und der typischen relativen Größe zu den Speicherbausteinen verwendet, auf die eine gesunde Verwendung von Zeigern hinweisen wird.
Wo Zeiger teuer werden, sind nicht Zeiger selbst, sondern:
Dynamische Speicherzuordnung. Dynamische Speicherzuweisung ist in der Regel teuer, da sie eine zugrunde liegende Datenstruktur durchlaufen muss (z. B. Buddy- oder Plattenzuweisung). Auch wenn diese häufig zu Tode optimiert sind, sind sie universell einsetzbar und für die Verarbeitung von Blöcken mit variabler Größe konzipiert, für die mindestens ein bisschen Arbeit erforderlich ist, die einer "Suche" ähnelt (wenn auch mit geringem Gewicht und möglicherweise sogar mit konstanter Zeit) Suchen Sie einen freien Satz zusammenhängender Seiten im Speicher.
Speicherzugriff. Dies ist in der Regel der größere Aufwand, über den Sie sich Sorgen machen müssen. Wann immer wir zum ersten Mal auf dynamisch zugewiesenen Speicher zugreifen, gibt es einen obligatorischen Seitenfehler sowie Cache-Fehler, die den Speicher in der Speicherhierarchie nach unten und in ein Register verschieben.
Speicherzugriff
Der Speicherzugriff ist einer der kritischsten Aspekte der Leistung jenseits von Algorithmen. Viele leistungskritische Bereiche wie AAA-Game-Engines konzentrieren einen Großteil ihrer Energie auf datenorientierte Optimierungen, die auf effizientere Speicherzugriffsmuster und -layouts hinauslaufen.
Eine der größten Leistungsschwierigkeiten von übergeordneten Sprachen, die jeden benutzerdefinierten Typ separat über einen Garbage Collector zuordnen möchten, besteht beispielsweise darin, dass sie den Speicher ziemlich stark fragmentieren können. Dies kann insbesondere dann der Fall sein, wenn nicht alle Objekte gleichzeitig zugeordnet sind.
Wenn Sie in diesen Fällen eine Liste mit einer Million Instanzen eines benutzerdefinierten Objekttyps speichern, kann der sequenzielle Zugriff auf diese Instanzen in einer Schleife sehr langsam sein, da er einer Liste mit einer Million Zeigern entspricht, die auf unterschiedliche Speicherbereiche verweisen. In diesen Fällen möchte die Architektur Speicher von oberen, langsameren und größeren Hierarchieebenen in großen, ausgerichteten Blöcken abrufen, in der Hoffnung, dass auf die umgebenden Daten in diesen Blöcken vor der Räumung zugegriffen werden kann. Wenn jedes Objekt in einer solchen Liste separat zugewiesen wird, wird es häufig mit Cache-Fehlern bezahlt, wenn jede nachfolgende Iteration möglicherweise aus einem völlig anderen Bereich im Speicher geladen werden muss, ohne dass auf benachbarte Objekte vor der Räumung zugegriffen wird.
Viele der Compiler für solche Sprachen leisten heutzutage sehr gute Arbeit bei der Auswahl von Befehlen und der Zuweisung von Registern, aber der Mangel an direkterer Kontrolle über die Speicherverwaltung kann tödlich sein (wenn auch oft weniger fehleranfällig) und dennoch Sprachen wie C und C ++ sehr beliebt.
Indirekte Optimierung des Zeigerzugriffs
In den leistungskritischsten Szenarien verwenden Anwendungen häufig Speicherpools, die Speicher aus zusammenhängenden Abschnitten bündeln, um die Referenzlokalität zu verbessern. In solchen Fällen kann sogar eine verknüpfte Struktur wie ein Baum oder eine verknüpfte Liste cachefreundlich gemacht werden, vorausgesetzt, das Speicherlayout ihrer Knoten ist zusammenhängender Natur. Dadurch wird die Dereferenzierung von Zeigern effektiv verbilligt, wenn auch indirekt, indem die Referenzlokalität verbessert wird, die bei der Dereferenzierung einbezogen wird.
Verfolgen von Zeigern
Angenommen, wir haben eine einfach verknüpfte Liste wie:
Foo->Bar->Baz->null
Das Problem ist, dass, wenn wir alle diese Knoten einem Allzweck-Allokator zuordnen (und möglicherweise nicht alle auf einmal), der tatsächliche Speicher möglicherweise in etwa so verteilt ist (vereinfachtes Diagramm):
Wenn wir anfangen, Zeiger zu verfolgen und auf den Foo
Knoten zuzugreifen , beginnen wir mit einem obligatorischen Fehler (und möglicherweise einem Seitenfehler), der einen Block aus seinem Speicherbereich von langsameren Speicherbereichen in schnellere Speicherbereiche verschiebt, wie folgt:
Dies bewirkt, dass wir einen Speicherbereich (möglicherweise auch eine Seite) zwischenspeichern, um nur auf einen Teil davon zuzugreifen und den Rest zu entfernen, während wir Zeiger um diese Liste herum verfolgen. Indem wir jedoch die Kontrolle über den Speicherzuweiser übernehmen, können wir eine solche Liste zusammenhängend wie folgt zuweisen:
... und damit die Geschwindigkeit, mit der wir diese Zeiger dereferenzieren und ihre Spitzenwerte verarbeiten können, erheblich verbessern. Obwohl sehr indirekt, können wir den Zeigerzugriff auf diese Weise beschleunigen. Wenn wir diese nur zusammenhängend in einem Array speichern würden, wäre dieses Problem natürlich nicht an erster Stelle zu sehen, aber der hier angegebene Speicherzuweiser, der uns die explizite Kontrolle über das Speicherlayout gibt, kann den Tag retten, an dem eine verknüpfte Struktur erforderlich ist.
* Hinweis: Dies ist ein sehr vereinfachtes Diagramm und eine Diskussion über die Speicherhierarchie und die Referenzlokalität, aber hoffentlich ist es für die Ebene der Frage angemessen.