Warum können Arrays nicht zugeschnitten werden?

70

Auf der MSDN-Dokumentationssite wird Folgendes zur Array.ResizeMethode angegeben:

Wenn newSize größer als die Länge des alten Arrays ist, wird ein neues Array zugewiesen und alle Elemente werden vom alten Array in das neue kopiert.

Wenn newSize kleiner als die Länge des alten Arrays ist, wird ein neues Array zugewiesen und Elemente werden vom alten Array in das neue kopiert, bis das neue gefüllt ist. Die restlichen Elemente im alten Array werden ignoriert.

Ein Array ist eine Folge benachbarter Speicherblöcke. Wenn wir ein größeres Array benötigen, können wir meines Wissens keinen Speicher hinzufügen, da der Speicher daneben möglicherweise bereits von anderen Daten beansprucht wird. Wir müssen also eine neue Folge benachbarter Speicherblöcke mit der gewünschten größeren Größe beanspruchen, unsere Einträge dort kopieren und unseren Anspruch auf den alten Speicherplatz entfernen.

Aber warum ein neues Array mit kleinerer Größe erstellen? Warum kann das Array nicht einfach seinen Anspruch auf die letzten Speicherblöcke entfernen? Dann wäre es eine O (1) -Operation anstelle von O (n), wie es jetzt ist.

Hat dies etwas damit zu tun, wie die Daten auf Computerarchitektur- oder physischer Ebene organisiert sind?

Kjara
quelle
2
Traditionell bewegt sich mit Cs mallocund reallocmanchmal reallocObjekte, manchmal nicht. Einer der Gründe dafür ist manchmal, dass der Speichermanager über einen separaten Pool für kleine Speicherblöcke verfügt. Wenn Sie einen großen Speicherblock auf einen kleinen reduzieren, muss er möglicherweise in den separaten Pool verschoben werden. Aber ich kann nicht sagen, ob es für .NET dasselbe ist.
1
Obwohl dies eine Spekulation ist, lässt die Zuweisung von Speicherplatz an einem neuen Speicherort die Möglichkeit, den Speicherort auszuwählen, um die Speicherfragmentierung zu minimieren, indem der neue Speicherort ausgewählt wird, an den das Array am besten passt.
Codor
1
@Codor Erstellt dies nicht ein größeres leeres Loch (Fragment) an der ursprünglichen Position des Arrays?
Zein Makki
5
Der Heap in C # hat absolut keine Ähnlichkeit mit einem C- oder C ++ - Heap. C # ist Müll gesammelt.
Doug65536
2
Ein einzelnes Byte wird immer nur dann auf dem Heap gespeichert, wenn es eingerahmt ist. Es dauert dann mindestens 12 Bytes anstelle von 1. ArrayList zu einer unbeliebten Klasse gemacht :)
Hans Passant

Antworten:

22

Um Ihre Frage zu beantworten, hat dies mit dem Design des Speicherverwaltungssystems zu tun.

Wenn Sie Ihr eigenes Speichersystem schreiben, können Sie es theoretisch so gestalten, dass es sich genau so verhält, wie Sie es gesagt haben.

Die Frage wird dann, warum es nicht so entworfen wurde. Die Antwort ist, dass das Speicherverwaltungssystem einen Kompromiss zwischen effizienter Speichernutzung und Leistung eingegangen ist.

Beispielsweise verwalten die meisten Speicherverwaltungssysteme den Speicher nicht bis auf das Byte. Stattdessen teilen sie den Speicher in 8-KB-Blöcke auf. Dafür gibt es eine Reihe von Gründen, von denen sich die meisten auf die Leistung beziehen.

Einige der Gründe haben damit zu tun, wie gut der Prozessor den Speicher bewegt. Nehmen wir zum Beispiel an, der Prozessor konnte 8 KB Daten gleichzeitig viel besser kopieren als 4 KB. Das Speichern der Daten in 8-KB-Blöcken bietet dann einen Leistungsvorteil. Das wäre ein Design-Kompromiss, der auf der CPU-Architektur basiert.

Es gibt auch algorithmische Leistungskompromisse. Wenn Sie beispielsweise das Verhalten der meisten Anwendungen untersuchen, stellen Sie fest, dass Anwendungen in 99% der Fälle Datenblöcke mit einer Größe von 6 KB bis 8 KB zuweisen.

Wenn das Speichersystem es Ihnen erlauben würde, 4 KB zuzuweisen und freizugeben, würde ein freier 4 KB-Block übrig bleiben, den 99% der Zuweisungen nicht verwenden können. Wenn nicht zu viel 8 KB zugewiesen würden, obwohl nur 4 KB benötigt würden, wäre es viel wiederverwendbarer.

Betrachten Sie noch ein anderes Design. Angenommen, Sie hatten eine Liste mit freien Speicherplätzen, die beliebig groß sein können, und es wurde eine Anforderung zum Zuweisen von 2 KB Speicher gestellt. Ein Ansatz wäre, Ihre Liste des freien Speichers zu durchsuchen und einen zu finden, der mindestens 2 KB groß ist. Sehen Sie sich jedoch die gesamte Liste an, um den kleinsten Block zu finden, oder finden Sie den ersten, der groß genug ist und verwendet wird Das.

Der erste Ansatz ist effizienter, aber langsamer, der zweite Ansatz ist weniger effizient, aber schneller.

Noch interessanter wird es in Sprachen wie C # und Java, die "verwalteten Speicher" haben. In einem verwalteten Speichersystem wird der Speicher nicht einmal freigegeben. es hört einfach auf, sich daran zu gewöhnen, was der Müllsammler später, in einigen Fällen viel später, erkennt und befreit.

Weitere Informationen zur Speicherverwaltung und -zuordnung finden Sie in diesem Artikel auf Wikipedia:

https://en.wikipedia.org/wiki/Memory_management

Luis Perez
quelle
Wenn das System es Ihnen erlaubt, das Ende eines zugewiesenen Blocks "freizugeben", würden Sie am Ende viel mehr "ungerade Blöcke" haben, die die "freie Liste" überladen - z. B. Sie haben 7k zugewiesen und sie auf "gekürzt" 5k, Sie würden mit einem 2k freien Block übrig bleiben, der wahrscheinlich die meisten zukünftigen Zuweisungen nicht erfüllen wird. Nach vielen solchen "Trims" haben Sie vielleicht viel freien Speicher, aber alles ist in kleinen (2-3k) Blöcken und meistens unbrauchbar. Durch die Neuzuweisung auf eine "Trimmung" werden diese verbleibenden Blöcke minimiert.
TripeHound
4
Dies ist zwar eine anständige Beschreibung von malloc / realloc, erklärt aber nicht wirklich, wie es in C # funktioniert (was die Frage des OP war). Derzeit wird MS.NET (genau wie bei einem Stapel) vom oberen Rand des Heapspeichers aus zugewiesen und verwendet die Heap-Komprimierung, um Speicher freizugeben. Der wahre Grund ist Sicherheit und Korrektheit, nicht Heap-Zuordnungsmuster.
Luaan
@Luaan Ich denke, es ist impliziert, dass der Kern der Kjara-Frage breiter ist als nur C #. In der Frage erwähnt er breite Konzepte wie "Speicherblöcke" und ob der Grund auf "Computerarchitektur oder physikalische Ebene" zurückzuführen ist. Sie haben Recht, dass .NET einen Heap verwendet. Ich würde jedoch argumentieren, dass dies ein Speicherzuweisungsmuster ist. Ich würde auch argumentieren, dass "teilweise Freigabe" in dieses Muster eingebaut werden könnte. Die einzige konkrete Antwort, die ich geben könnte, warum Sie dies in C # nicht tun können, ist, dass .NET nicht dafür ausgelegt ist, dies zu unterstützen. Warum wurde es nicht unterstützt? Wir konnten nur spekulieren.
Luis Perez
Es ist keine genaue Antwort. Es ruft nicht spezifizierte Magie hinter dem 8KB-Wert auf, keiner existiert. Sie sprechen vielleicht über die Seitengröße des virtuellen Speichers, sie beträgt 8 KB auf einem Itanium. Das hat sehr wenig mit effizientem Speicherzugriff zu tun, die Prozessor-Caches spielen eine viel, viel größere Rolle. Insbesondere L1, die erste, deren Speichereinheit 64 Bytes beträgt. Die Datenausrichtung ist der Schlüssel, um zu vermeiden, dass die Caches ineffizient verwendet werden. Dieses Detail ist in .NET sehr gut versteckt und wird in der StructLayoutAtttribute.Pack-Eigenschaft um die Ecke angezeigt.
Hans Passant
35

Nicht verwendeter Speicher wird nicht tatsächlich nicht verwendet. Es ist die Aufgabe jeder Heap-Implementierung, die Löcher im Heap zu verfolgen. Der Manager muss mindestens die Größe des Lochs kennen und den Standort verfolgen. Das kostet immer mindestens 8 Bytes.

In .NET spielt System.Object eine Schlüsselrolle. Jeder weiß, was es tut, was nicht so offensichtlich ist, dass es weiterlebt, nachdem ein Objekt gesammelt wurde. Die zwei zusätzlichen Felder im Objektheader (Syncblock und Typhandle) werden dann zu einem Rückwärts- und Vorwärtszeiger auf den vorherigen / nächsten freien Block. Es hat auch eine Mindestgröße von 12 Bytes im 32-Bit-Modus. Garantiert, dass immer genügend Speicherplatz vorhanden ist, um die freie Blockgröße nach dem Sammeln des Objekts zu speichern.

Sie sehen das Problem jetzt wahrscheinlich. Die Reduzierung der Größe eines Arrays garantiert nicht, dass ein Loch erstellt wird, das groß genug ist, um in diese drei Felder zu passen. Nichts, was es tun könnte, als eine Ausnahme "kann das nicht" auszulösen. Kommt auch auf die Bitness des Prozesses an. Völlig zu hässlich, um darüber nachzudenken.

Hans Passant
quelle
1
"Nichts, was es tun könnte, außer eine" kann diese "Ausnahme nicht auslösen" - nun, mit Sorgfalt können Sie definieren, dass es in diesem Fall nichts anderes tut, als das Feld zu aktualisieren, in dem die Größe des Arrays gespeichert ist. In diesen Fällen, in denen Sie die Größe des Arrays nur geringfügig reduzieren, besteht schließlich die Möglichkeit, dass die neue Speicherzuordnung, in die es kopiert wird, genauso viel Speicher verbraucht wie die alte. Ich denke, die vollständige Antwort ist etwas mehr als dies, aber es hängt davon ab, wie weit man bereit ist, das Systemdesign zu entwirren, bevor man es mit Trimmfunktionen neu entwirrt.
Steve Jessop
3
Es ist etwas mehr als das, ein Array hat kein "Kapazitäts" -Feld. Das Hinzufügen dieses Felds zu jedem Array-Objekt für eine solche Nebenfunktion ist sehr schwer zu verkaufen. Die Effizienz eines Arrays wirkt sich auf alles aus . Alle Sammlungstypen außer LinkedList verwenden Arrays unter der Haube.
Hans Passant
Oh, wenn Sie also nach einem kleinen Array wie 2 Elementen fragen, gibt Ihnen .NET eine Speicherzuordnung, die genau groß genug dafür ist, ohne Spiel am Ende und daher ohne ein vom Speicherzuweiser getrenntes Größenfeld Vorstellung davon, wie groß der Block ist? Und ich sage: "Es gibt jede Chance, dass die neue Speicherzuordnung, in die sie kopiert wird, tatsächlich genauso viel Speicher verbraucht."
Steve Jessop
6
@Steve Jessop: Eine veränderbare Arraygröße wirkt sich auf die Arbeit des Optimierers aus. Dies hängt von der Unveränderlichkeit der Länge ab, wenn vorhergesagt wird, ob eine Schleife die Arraygrenze überschreiten könnte. Eine veränderbare Größe bedeutet, dass die Bedingungen in jeder Iteration jeder Schleife erneut überprüft werden müssen . Dieser Leistungsabfall würde den Vorteil überwiegen, die Größe ohne Kopieren ändern zu können (in einigen Fällen).
Holger
@Holger: fair genug, obwohl wenn das der wahre Grund ist, was ist dann mit dem, was Hans sagt? Ich werde keinen von uns mit müßigen Spekulationen darüber belästigen, welcher Anteil der Zeitdatenflussanalyse zum Heben von Schleifeninvarianten verwendet werden kann: einige, aber keineswegs alle.
Steve Jessop
21

Ich habe nach einer Antwort auf Ihre Frage gesucht, da ich sie als sehr interessante Frage empfand. Ich habe diese Antwort gefunden , die eine interessante erste Zeile hat:

Sie können einen Teil eines Arrays nicht freigeben - Sie können nur free()einen Zeiger freigeben, von dem Sie ihn erhalten haben, malloc()und wenn Sie dies tun, geben Sie die gesamte von Ihnen angeforderte Zuordnung frei.

Das eigentliche Problem ist also das Register, das festhält, welcher Speicher zugewiesen ist. Sie können nicht einfach einen Teil des Blocks freigeben, den Sie zugewiesen haben, Sie müssen ihn vollständig freigeben oder Sie geben ihn überhaupt nicht frei. Das heißt, um diesen Speicher freizugeben, müssen Sie zuerst die Daten verschieben. Ich weiß nicht, ob die .NET-Speicherverwaltung diesbezüglich etwas Besonderes tut, aber ich denke, diese Regel gilt auch für die CLR.

Patrick Hofman
quelle
1
Beachten Sie, dass diese Antwort auf eine C-Antwort verweist, nicht auf eine C # -Antwort. In C # gibt es keinen Aufruf zu "malloc" hinter den Kulissen.
MSalters
1
Ich glaube nicht, Malloc geht zur CRT (C RunTime), die wiederum das Betriebssystem fragen muss. Ich denke, die CLR geht direkt zum Betriebssystem.
MSalters
2
Ja? Wenn Sie HeapAllocdie Standard-Windows-Funktion aufrufen möchten, die die CLR voraussichtlich verwenden wird, müssen Sie diesen Aufruf in einer bestimmten Sprache schreiben. Da Microsoft keinen C-Compiler unterhält (MSVC ++ steckt im 20. Jahrhundert fest und unterstützt nicht einmal C99), ist die logische Wahl C ++. Sie können diesen Aufruf jedoch auch über unsicheren C # -Code tätigen.
MSalters
10
Diese Kommentare sind nicht sehr genau. std :: shared_pointer ist der grundlegendste Grund, warum C # -Code C ++ - Code übertrifft, wenn die Speichernutzung der Engpass ist. Die Amortisation der Kosten für die Speicherbarrieren bringt es voran. Die CLR verwendet nicht HeapAlloc, sondern nur VirtualAlloc. VS2015 unterstützt C99 gut, was für die C ++ 11-Konformität erforderlich ist. Sie haben das Front-End komplett neu geschrieben, das Original wurde für die Kompilierung mit nur 256 KB Speicher entwickelt. Gut genug, um die CLR zu kompilieren :)
Hans Passant
2
Hans beschreibt die (ganz anderen) Probleme, mit denen die .NET-Laufzeit mit einer solchen Operation konfrontiert ist, in seiner Antwort recht gut - es ist ähnlich free, aber nicht wirklich. Sie könnten einen Teil des Speichers freigeben, aber Sie müssen die alte Objektreferenz beibehalten, da sie beim Sammeln des Objekts zu einem "freien Zeiger" wird. Dies bedeutet, dass Sie ohnehin keine direkte Größenänderung vornehmen können - selbst wenn genügend freier Speicherplatz für den nächsten Objektheader vorhanden ist, müssen Sie den Anfang des Arrays verschieben, was bedeutet, dass alle Elemente kopiert werden. Das Zuweisen eines neuen Arrays ist einfach rundum besser.
Luaan
6

Ich denke, das liegt daran, dass das alte Array nicht zerstört wird. Es ist immer noch da, wenn auf etwas anderes verwiesen wird und immer noch darauf zugegriffen werden kann. Aus diesem Grund wird das neue Array an einem neuen Speicherort erstellt.

Beispiel:

int[] original = new int[] { 1, 2, 3, 4, 5, 6 };
int[] otherReference = original; // currently points to the same object

Array.Resize(ref original, 3);

Console.WriteLine("---- OTHER REFERENCE-----");

for (int i = 0; i < otherReference.Length; i++)
{
    Console.WriteLine(i);
}

Console.WriteLine("---- ORIGINAL -----");

for (int i = 0; i < original.Length; i++)
{
    Console.WriteLine(i);
}

Drucke:

---- OTHER REFERENCE-----
0
1
2
3
4
5
---- ORIGINAL -----
0
1
2
Zein Makki
quelle
1
int[] oldRef = oldkopiert die Referenz (dh eine flache Kopie) in das Array.
CadentOrange
@ CadentOrange genau das habe ich gemeint.
Zein Makki
1
Dies ist keine Antwort auf meine Frage "Warum wird das Array überhaupt kopiert, wenn es nicht zum Kopieren benötigt wird?". Es zeigt nur die Konsequenzen des Kopierens des Arrays anstelle des Zuschneidens.
Kjara
3
@ user3185569 Das weiß ich. Und meine Frage ist, warum es keine Trimmfunktion gibt.
Kjara
1
@ Kjara Ich glaube nicht, dass du den Geist der Antwort verstanden hast. Darin heißt es: "Sie können die Größe des vorhandenen Arrays nicht ändern, da möglicherweise jemand anderes zur gleichen Zeit dasselbe Array verwendet, obwohl sich die Länge unter der Abdeckung nicht ändert ." Speichersicherheit ist in .NET eine große Sache - wie könnten Sie Speichersicherheit haben, wenn Sie Pufferüberläufe nur durch gut gestaltete Array.Resizes verursachen könnten ? :)
Luaan
5

Es gibt zwei Gründe für die Definition von Realloc wie es ist: Erstens wird absolut klar, dass es keine Garantie dafür gibt, dass der Aufruf von Realloc mit einer kleineren Größe denselben Zeiger zurückgibt. Wenn Ihr Programm diese Annahme macht, ist Ihr Programm fehlerhaft. Auch wenn der Zeiger in 99,99% der Fälle gleich ist. Wenn sich mitten in viel leerem Raum ein großer Block befindet, der zu einer Fragmentierung des Heapspeichers führt, kann realloc ihn nach Möglichkeit aus dem Weg räumen.

Zweitens gibt es Implementierungen, bei denen dies unbedingt erforderlich ist. Zum Beispiel hat MacOS X eine Implementierung, bei der ein großer Speicherblock verwendet wird, um Malloc-Blöcke von 1 bis 16 Bytes zuzuweisen, ein anderer großer Speicherblock für Malloc-Blöcke von 17 bis 32 Bytes, einer für Malloc-Blöcke von 33 bis 48 Bytes usw. Dies macht es sehr natürlich, dass jede Größenänderung, die im Bereich von 33 bis 48 Bytes bleibt, denselben Block zurückgibt, aber das Ändern auf 32 oder 49 Bytes muss den Block neu zuweisen.

Es gibt keine Garantie für die Leistung von Realloc. In der Praxis wird die Größe jedoch nicht ein bisschen kleiner. Die Hauptfälle sind: Ordnen Sie Speicher einer geschätzten Obergrenze der erforderlichen Größe zu, füllen Sie ihn aus und ändern Sie die Größe auf die tatsächlich viel kleinere erforderliche Größe. Oder weisen Sie Speicher zu und ändern Sie die Größe auf etwas sehr Kleines, wenn es nicht mehr benötigt wird.

gnasher729
quelle
5

Nur die Designer der .NET-Laufzeit können Ihnen ihre tatsächlichen Gründe mitteilen. Ich vermute jedoch, dass die Speichersicherheit in .NET von größter Bedeutung ist und es sehr teuer wäre, sowohl die Speichersicherheit als auch die veränderlichen Array-Längen aufrechtzuerhalten, ganz zu schweigen davon, wie kompliziert Code mit Arrays wäre.

Betrachten Sie den einfachen Fall:

var fun = 0;
for (var i = 0; i < array.Length; i++)
{
  fun ^= array[i];
}

Um die Speichersicherheit zu gewährleisten, arraymuss jeder Zugriff auf Grenzen überprüft werden, während sichergestellt werden muss, dass die Grenzen nicht von anderen Threads überprüft werden (die .NET-Laufzeit hat viel strengere Garantien als beispielsweise der C-Compiler).

Sie benötigen also eine thread-sichere Operation, die Daten aus dem Array liest und gleichzeitig die Grenzen überprüft. Es gibt keine solche Anweisung auf der CPU, daher ist Ihre einzige Option ein Synchronisationsprimitiv. Ihr Code wird zu:

var fun = 0;
for (var i = 0; i < array.Length; i++)
{
  lock (array)
  {
    if (i >= array.Length) throw new IndexOutOfBoundsException(...);

    fun ^= array[i];
  }
}

Das ist natürlich schrecklich teuer. Wenn Sie die Array-Länge unveränderlich machen, erhalten Sie zwei massive Leistungssteigerungen:

  • Da sich die Länge nicht ändern kann, muss die Begrenzungsprüfung nicht synchronisiert werden. Dies macht jede einzelne Grenzüberprüfung erheblich billiger.
  • ... und Sie können die Grenzen weglassen, um zu überprüfen, ob Sie die Sicherheit dafür nachweisen können.

In Wirklichkeit ist das, was die Laufzeit tatsächlich tut, eher so:

var fun = 0;
var len = array.Length; // Provably safe

for (var i = 0; i < len; i++)
{
  // Provably safe, no bounds checking needed
  fun ^= array[i];
}

Am Ende haben Sie eine enge Schleife, die sich nicht von der in C unterscheidet - aber gleichzeitig ist sie absolut sicher.

Lassen Sie uns nun die Vor- und Nachteile des Hinzufügens eines Arrays so sehen, wie Sie es möchten:

Vorteile:

  • In dem sehr seltenen Szenario, in dem Sie ein Array verkleinern möchten, bedeutet dies, dass das Array nicht kopiert werden muss, um seine Länge zu ändern. Es würde jedoch auch in Zukunft eine Heap-Komprimierung erfordern, die viel Kopieren erfordert.
  • Wenn Sie Objektreferenzen im Array speichern, können Sie einige Vorteile aus der Cache-Lokalität ziehen, wenn die Zuordnung des Arrays und der Elemente zufällig zusammengeführt wird. Dies ist natürlich noch seltener als Pro # 1.

Nachteile:

  • Jeder Array-Zugriff würde selbst in engen Schleifen schrecklich teuer werden. Also würde jeder unsafestattdessen Code verwenden, und da geht Ihre Speichersicherheit.
  • Jeder einzelne Code, der sich mit Arrays befasst, muss damit rechnen, dass sich die Länge des Arrays jederzeit ändern kann. Jeder einzelne Array-Zugriff würde ein benötigen try ... catch (IndexOutOfRangeException), und jeder, der über ein Array iteriert, müsste in der Lage sein, mit der sich ändernden Größe umzugehen - jemals gefragt, warum Sie keine Elemente hinzufügen oder entfernen könnenList<T> Sie iterieren?
  • Eine enorme Menge an Arbeit für das CLR-Team, die nicht für eine andere, wichtigere Funktion verwendet werden konnte.

Es gibt einige Implementierungsdetails, die dies noch weniger vorteilhaft machen. Am wichtigsten ist, dass .NET-Heap nichts mit malloc/ freepattern zu tun hat . Wenn wir den LOH ausschließen, verhält sich der aktuelle MS.NET-Heap ganz anders:

  • Zuordnungen erfolgen immer von oben, wie in einem Stapel. Dies macht die Zuweisung im Gegensatz dazu fast so billig wie die Stapelzuweisung malloc.
  • Aufgrund der Zuordnungsmuster, um tatsächlich „frei“ Speicher, müssen Sie verdichten die Haufen nach tut eine Sammlung. Dadurch werden Objekte so verschoben, dass die freien Speicherplätze im Heap gefüllt werden. Dadurch wird die "Oberseite" des Heaps niedriger, sodass Sie mehr Objekte im Heap zuweisen oder einfach den Speicher für die Verwendung durch andere Anwendungen auf dem System freigeben können .
  • Um die Cache-Lokalität beizubehalten (unter der Annahme, dass Objekte, die üblicherweise zusammen verwendet werden, auch nahe beieinander zugewiesen werden, was eine gute Annahme ist), muss möglicherweise jedes einzelne Objekt im Heap über dem freigegebenen Speicherplatz nach unten verschoben werden. Sie haben sich vielleicht eine Kopie eines 100-Byte-Arrays gespeichert, müssen dann aber trotzdem 100 MiB anderer Objekte verschieben.

Darüber hinaus bedeutet, wie Hans in seiner Antwort sehr gut erklärte, nur weil das Array kleiner ist, nicht unbedingt, dass aufgrund der Objektheader genügend Platz für ein kleineres Array in der gleichen Speichermenge vorhanden ist (denken Sie daran, wie .NET dafür ausgelegt ist Speichersicherheit? Die Kenntnis des richtigen Objekttyps ist ein Muss für die Laufzeit. Er weist jedoch nicht darauf hin, dass Sie das Array auch dann verschieben müssen, wenn Sie über genügend Speicher verfügen . Betrachten Sie ein einfaches Array:

ObjectHeader,1,2,3,4,5

Jetzt entfernen wir die letzten beiden Elemente:

OldObjectHeader;NewObjectHeader,1,2,3

Hoppla. Wir benötigen den alten Objektheader, um die Freiraumliste beizubehalten, da wir den Heap sonst nicht richtig komprimieren könnten. Nun könnte es möglich sein, den alten Objektheader über das Array hinaus zu verschieben, um die Kopie zu vermeiden, aber das ist noch eine weitere Komplikation. Dies stellt sich als ziemlich teures Feature für etwas heraus, das noöne jemals wirklich verwenden wird.

Und das ist alles noch in der verwalteten Welt. Mit .NET können Sie jedoch bei Bedarf auf unsicheren Code zurückgreifen - beispielsweise bei der Interaktion mit nicht verwaltetem Code. Wenn Sie Daten an eine native Anwendung übergeben möchten, haben Sie zwei Möglichkeiten: Sie können entweder das verwaltete Handle anheften, um zu verhindern, dass es erfasst und verschoben wird, oder Sie kopieren die Daten. Wenn Sie einen kurzen, synchronen Anruf tätigen, ist das Fixieren sehr billig (obwohl gefährlicher - der native Code hat keine Sicherheitsgarantien). Das Gleiche gilt beispielsweise für die Bearbeitung von Daten in einer engen Schleife wie bei der Bildverarbeitung - das Kopieren der Daten ist eindeutig keine Option. Wenn Sie Array.Resizedas vorhandene Array ändern dürfen , wird dies vollständig unterbrochenArray.Resize Sie müssen überprüfen, ob dem Array, dessen Größe Sie ändern möchten, ein Handle zugeordnet ist, und in diesem Fall eine Ausnahme auslösen.

Weitere Komplikationen, über die man viel schwerer nachdenken kann (Sie werden jede Menge Spaß daran haben, den Fehler zu verfolgen, der nur gelegentlich auftritt, wenn der Array.ResizeVersuch, die Größe eines Arrays zu ändern, das gerade passiert, festgesteckt wird Erinnerung).

Wie andere erklärt haben, ist nativer Code nicht viel besser. Sie müssen zwar nicht dieselben Sicherheitsgarantien einhalten (was ich nicht wirklich als Vorteil nutzen würde, aber na ja), aber es gibt immer noch Komplikationen in Bezug auf die Art und Weise, wie Sie Speicher zuweisen und verwalten. Wird aufgerufen realloc, um ein Array mit 10 Elementen und 5 Elementen zu erstellen? Nun, es wird entweder kopiert oder es wird immer noch die Größe eines Arrays mit 10 Elementen haben, da es keine Möglichkeit gibt, den verbleibenden Speicher auf vernünftige Weise zurückzugewinnen.

Um es kurz zusammenzufassen: Sie fragen nach einer sehr teuren Funktion, die in einem äußerst seltenen Szenario nur von sehr begrenztem Nutzen ist (falls vorhanden) und für die es eine einfache Problemumgehung gibt (Erstellen einer eigenen Array-Klasse). . Ich sehe nicht, dass die Messlatte für "Sicher, lassen Sie uns diese Funktion implementieren!" :) :)

Luaan
quelle
+1 nette Antwort. Ich habe vor einiger Zeit im Java-Bereich ( stackoverflow.com/a/35847639/5851520 ) auf ähnliche Fragen geantwortet , aber Ihr Detail ist cool.
SusanW
3

In jedem Heap-Management-System gibt es möglicherweise viele ausgefeilte Datenstrukturen, die "unter der Haube" arbeiten. Sie können beispielsweise Blöcke entsprechend ihrer aktuellen Größe speichern. Es würde eine Menge Komplikationen hinzufügen, wenn Blöcke "geteilt, wachsen und schrumpfen" könnten. (Und es würde die Dinge wirklich nicht schneller machen.)

Daher ist die Implementierung immer sicher : Sie weist einen neuen Block zu und verschiebt die Werte nach Bedarf. Es ist bekannt, dass "diese Strategie auf jedem System immer zuverlässig funktioniert". Und es wird die Dinge wirklich überhaupt nicht verlangsamen.

Mike Robinson
quelle
2

Unter der Haube werden Arrays in einem kontinuierlichen Speicherblock gespeichert, sind jedoch in vielen Sprachen immer noch ein primitiver Typ.

Um Ihre Frage zu beantworten, wird der einem Array zugewiesene Speicherplatz als ein einzelner Block betrachtet und stackbei lokalen Variablen oder bss/data segmentsbei globalen Variablen gespeichert . AFAIK: Wenn Sie auf ein Array wie array[3]auf niedriger Ebene zugreifen , erhalten Sie vom OS einen Zeiger auf das erste Element und springen / überspringen, bis es (im obigen Beispiel dreimal) den erforderlichen Block erreicht. Es kann also eine architektonische Entscheidung sein, dass eine Arraygröße nach ihrer Deklaration nicht mehr geändert werden kann.

In ähnlicher Weise kann das Betriebssystem nicht wissen, ob es ein gültiger Index eines Arrays ist, bevor es auf den erforderlichen Index zugreift. Wenn es versucht, auf den angeforderten Index zuzugreifen, indem es nach dem jumpingProzess den Speicherblock erreicht, und feststellt, dass der erreichte Speicherblock nicht Teil des Arrays ist, wird ein ausgelöstException

Vishnu Prasad V.
quelle
2
Wie stellt das Betriebssystem fest, ob der Speicherblock nicht Teil des Arrays ist? Muss es nicht nur den Startpunkt des Arrays, sondern auch seine Größe kennen? In diesem Fall wäre das Ändern des gespeicherten Werts der Größe (der, wie ich aus Ihrer Antwort verstehe, auf dem Stapel liegt) alles, was zu tun ist, oder?
Kjara