C ++ hat std :: vector und Java hat ArrayList, und viele andere Sprachen haben ihre eigene Form eines dynamisch zugewiesenen Arrays. Wenn einem dynamischen Array der Speicherplatz ausgeht, wird es einem größeren Bereich neu zugewiesen und die alten Werte werden in das neue Array kopiert. Eine zentrale Frage für die Leistung eines solchen Arrays ist, wie schnell das Array an Größe zunimmt. Wenn Sie immer nur groß genug werden, um dem aktuellen Push zu entsprechen, werden Sie jedes Mal neu zugewiesen. Es ist also sinnvoll, die Array-Größe zu verdoppeln oder mit etwa dem 1,5-fachen zu multiplizieren.
Gibt es einen idealen Wachstumsfaktor? 2x? 1,5x? Mit Ideal meine ich mathematisch begründete, beste Ausgleichsleistung und verschwendeten Speicher. Ich erkenne, dass theoretisch, da Ihre Anwendung eine mögliche Verteilung von Pushs haben könnte, dies etwas anwendungsabhängig ist. Aber ich bin gespannt, ob es einen Wert gibt, der "normalerweise" am besten ist oder innerhalb einer strengen Einschränkung als der beste angesehen wird.
Ich habe gehört, dass es irgendwo ein Papier darüber gibt, aber ich konnte es nicht finden.
Idealerweise (im Grenzwert als n → ∞) ist es der goldene Schnitt : ϕ = 1,618 ...
In der Praxis möchten Sie etwas Nahes wie 1.5.
Der Grund dafür ist, dass Sie ältere Speicherblöcke wiederverwenden, das Caching nutzen und vermeiden möchten, dass das Betriebssystem Ihnen ständig mehr Speicherseiten zur Verfügung stellt. Die Gleichung, die Sie lösen würden, um dies sicherzustellen, reduziert sich auf x n - 1 - 1 = x n + 1 - x n , dessen Lösung sich x = ϕ für großes n nähert .
quelle
Ein Ansatz bei der Beantwortung solcher Fragen besteht darin, nur zu "schummeln" und zu betrachten, was populäre Bibliotheken tun, unter der Annahme, dass eine weit verbreitete Bibliothek zumindest nichts Schreckliches tut.
Ruby (1.9.1-p129) verwendet beim Anhängen an ein Array nur das 1,5-fache und Python (2.6.2) das 1,125-fache plus eine Konstante (in
Objects/listobject.c
):newsize
oben ist die Anzahl der Elemente im Array. Beachten Sie, dass diesnewsize
hinzugefügt wirdnew_allocated
, sodass der Ausdruck mit den Bitverschiebungen und dem ternären Operator wirklich nur die Überzuordnung berechnet.quelle
Angenommen, Sie vergrößern das Array um
x
. Nehmen wir also an, Sie beginnen mit der GrößeT
. Wenn Sie das Array das nächste Mal vergrößern, wird es größerT*x
. Dann wird es seinT*x^2
und so weiter.Wenn Sie den zuvor erstellten Speicher wiederverwenden möchten, möchten Sie sicherstellen, dass der neu zugewiesene Speicher geringer ist als die Summe des zuvor freigegebenen Speichers. Deshalb haben wir diese Ungleichung:
Wir können T von beiden Seiten entfernen. Also bekommen wir folgendes:
Informell sagen wir, dass bei der
nth
Zuweisung der gesamte zuvor freigegebene Speicher größer oder gleich dem Speicherbedarf bei der n-ten Zuweisung sein soll, damit wir den zuvor freigegebenen Speicher wiederverwenden können.Wenn wir dies beispielsweise im dritten Schritt (dh
n=3
) tun möchten, haben wirDiese Gleichung gilt für alle x so, dass
0 < x <= 1.3
(grob)Sehen Sie unten, welches x wir für verschiedene n erhalten:
Beachten Sie, dass der Wachstumsfaktor geringer sein muss als
2
seitdemx^n > x^(n-2) + ... + x^2 + x + 1 for all x>=2
.quelle
Es kommt wirklich darauf an. Einige Leute analysieren häufige Anwendungsfälle, um die optimale Anzahl zu finden.
Ich habe 1,5x 2,0x Phi x und eine Potenz von 2 gesehen.
quelle
Wenn Sie eine Verteilung über Array-Längen haben und über eine Utility-Funktion verfügen, die angibt, wie gerne Sie Speicherplatz oder Zeit verschwenden, können Sie auf jeden Fall eine optimale Strategie für die Größenänderung (und die anfängliche Größenänderung) auswählen.
Der Grund, warum das einfache konstante Vielfache verwendet wird, ist offensichtlich, dass jeder Anhang eine konstante Zeit amortisiert hat. Dies bedeutet jedoch nicht, dass Sie für kleine Größen kein anderes (größeres) Verhältnis verwenden können.
In Scala können Sie loadFactor für die Standardbibliotheks-Hash-Tabellen mit einer Funktion überschreiben, die die aktuelle Größe anzeigt. Seltsamerweise verdoppeln sich die veränderbaren Arrays nur, was die meisten Leute in der Praxis tun.
Ich kenne keine doppelten (oder 1,5 * ing) Arrays, die tatsächlich Speicherfehler auffangen und in diesem Fall weniger wachsen. Es scheint, dass Sie das tun möchten, wenn Sie ein riesiges einzelnes Array hätten.
Ich möchte weiter hinzufügen, dass es sinnvoll sein kann, die Größe der Arrays zu ändern, wenn Sie die Größe der Arrays lange genug beibehalten und den Speicherplatz im Laufe der Zeit bevorzugen erledigt.
quelle
Noch zwei Cent
old*3/2
sodass keine Gleitkommaoperationen erforderlich sind. (Ich sage,/2
weil Compiler es durch Bitverschiebung im generierten Assemblycode ersetzen, wenn sie es für richtig halten.)Wie von jemandem erwähnt, fühlt sich 2 besser an als 8. Und auch 2 fühlt sich besser an als 1.1.
Mein Gefühl ist, dass 1,5 ein guter Standard ist. Davon abgesehen hängt es vom konkreten Fall ab.
quelle
n + n/2
den Überlauf zu verzögern. Durchn*3/2
die Verwendung wird Ihre mögliche Kapazität halbiert.Ich stimme Jon Skeet zu, sogar mein theoretischer Freund besteht darauf, dass dies als O (1) nachgewiesen werden kann, wenn der Faktor auf 2x gesetzt wird.
Das Verhältnis zwischen CPU-Zeit und Speicher ist auf jedem Computer unterschiedlich, daher variiert der Faktor ebenso stark. Wenn Sie einen Computer mit Gigabyte RAM und einer langsamen CPU haben, ist das Kopieren der Elemente in ein neues Array viel teurer als auf einem schnellen Computer, der möglicherweise weniger Speicher hat. Diese Frage kann theoretisch für einen einheitlichen Computer beantwortet werden, was Ihnen in realen Szenarien überhaupt nicht hilft.
quelle
Ich weiß, dass es eine alte Frage ist, aber es gibt einige Dinge, die jeder zu vermissen scheint.
Erstens ist dies eine Multiplikation mit 2: Größe << 1. Dies ist eine Multiplikation mit etwas zwischen 1 und 2: int (Gleitkomma (Größe) * x), wobei x die Zahl ist, * Gleitkomma-Mathematik ist und der Prozessor hat um zusätzliche Anweisungen zum Gießen zwischen float und int auszuführen. Mit anderen Worten, auf Maschinenebene erfordert das Verdoppeln eine einzige, sehr schnelle Anweisung, um die neue Größe zu finden. Das Multiplizieren mit etwas zwischen 1 und 2 erfordert mindestenseine Anweisung zum Umwandeln der Größe in einen Float, eine Anweisung zum Multiplizieren (was eine Float-Multiplikation ist, sodass es wahrscheinlich mindestens doppelt so viele Zyklen dauert, wenn nicht vier- oder sogar achtmal so viele) und eine Anweisung zum Zurückwandeln in int. Dies setzt voraus, dass Ihre Plattform Float-Mathematik für die Allzweckregister durchführen kann, anstatt die Verwendung spezieller Register zu erfordern. Kurz gesagt, Sie sollten erwarten, dass die Mathematik für jede Zuordnung mindestens zehnmal so lange dauert wie eine einfache Linksverschiebung. Wenn Sie jedoch während der Neuzuweisung viele Daten kopieren, macht dies möglicherweise keinen großen Unterschied.
Zweitens und wahrscheinlich der große Kicker: Jeder scheint anzunehmen, dass der Speicher, der freigegeben wird, sowohl an sich selbst als auch an den neu zugewiesenen Speicher angrenzt. Dies ist mit ziemlicher Sicherheit nicht der Fall, es sei denn, Sie weisen den gesamten Speicher selbst vorab zu und verwenden ihn dann als Pool. Das Betriebssystem kann gelegentlichAm Ende tun Sie dies, aber die meiste Zeit wird es genug Fragmentierung des freien Speicherplatzes geben, damit jedes halbwegs anständige Speicherverwaltungssystem ein kleines Loch finden kann, in das Ihr Speicher gerade passt. Sobald Sie wirklich kleine Stücke bekommen, werden Sie mit größerer Wahrscheinlichkeit zusammenhängende Teile erhalten, aber bis dahin sind Ihre Zuweisungen groß genug, dass Sie sie nicht häufig genug ausführen, damit es nicht mehr darauf ankommt. Kurz gesagt, es macht Spaß, sich vorzustellen, dass die Verwendung einer idealen Zahl die effizienteste Nutzung des freien Speicherplatzes ermöglicht. In Wirklichkeit wird dies jedoch nur geschehen, wenn Ihr Programm auf Bare Metal ausgeführt wird (wie in, es gibt kein Betriebssystem) darunter alle Entscheidungen treffen).
Meine Antwort auf die Frage? Nein, es gibt keine ideale Zahl. Es ist so anwendungsspezifisch, dass es niemand wirklich versucht. Wenn Ihr Ziel die ideale Speichernutzung ist, haben Sie ziemlich viel Pech. Für die Leistung sind weniger häufige Zuweisungen besser, aber wenn wir nur so weitermachen, könnten wir mit 4 oder sogar 8 multiplizieren! Wenn Firefox auf einmal von 1 GB auf 8 GB springt, werden sich die Leute beschweren, so dass dies nicht einmal Sinn macht. Hier sind einige Faustregeln, nach denen ich mich richten würde:
Wenn Sie die Speichernutzung nicht optimieren können, verschwenden Sie zumindest keine Prozessorzyklen. Das Multiplizieren mit 2 ist mindestens eine Größenordnung schneller als das Gleitkomma-Rechnen. Es mag keinen großen Unterschied machen, aber es wird zumindest einen Unterschied machen (besonders früh, während der häufigeren und kleineren Zuteilungen).
Überdenken Sie es nicht. Wenn Sie nur 4 Stunden damit verbracht haben, herauszufinden, wie Sie etwas tun können, was bereits getan wurde, haben Sie nur Ihre Zeit verschwendet. Ganz ehrlich, wenn es eine bessere Option als * 2 gegeben hätte, wäre dies vor Jahrzehnten in der C ++ - Vektorklasse (und an vielen anderen Orten) geschehen.
Wenn Sie wirklich optimieren möchten, sollten Sie die kleinen Dinge nicht ins Schwitzen bringen. Heutzutage kümmert sich niemand mehr darum, dass 4 KB Speicher verschwendet werden, es sei denn, sie arbeiten an eingebetteten Systemen. Wenn Sie 1 GB Objekte zwischen 1 MB und 10 MB erreichen, ist das Verdoppeln wahrscheinlich viel zu viel (ich meine, das sind zwischen 100 und 1.000 Objekte). Wenn Sie die erwartete Expansionsrate schätzen können, können Sie sie an einem bestimmten Punkt auf eine lineare Wachstumsrate ausgleichen. Wenn Sie ungefähr 10 Objekte pro Minute erwarten, ist es wahrscheinlich in Ordnung, mit 5 bis 10 Objektgrößen pro Schritt (einmal alle 30 Sekunden bis zu einer Minute) zu wachsen.
Es kommt darauf an, nicht zu überlegen, zu optimieren, was Sie können, und bei Bedarf an Ihre Anwendung (und Plattform) anzupassen.
quelle
n + n >> 1
ist das gleiche wie1.5 * n
. Es ist ziemlich einfach, ähnliche Tricks für jeden praktischen Wachstumsfaktor zu finden, den Sie sich vorstellen können.