Rolf B: Garbage Collector verbessern/schneller machen

Beitrag lesen

Hallo Michael_K,

die push-Methode erweitert das Zielarray Stück für Stück um einen Eintrag. Ein dynamisches Array in Sprachen wie JavaScript ist kein klassisches Array, was aus einem zusammenhängenden Speicherbereich besteht, sondern ein Objekt, das numerische Schlüssel speziell behandelt. Also eigentlich: eine Map.

Wenn Du das neue Array als [] initialisierst, dann reserviert er Speicher für einige wenige Einträge. In .net passiert sowas beim ArrayList Typ, da sind es anfangs 8 Einträge. Wenn man dann einzeln Elemente anfügt, muss er den reservierten Speicher vergrößern. D.h. einen neuen Speicherblock bestellen, der mehr Platz hat, den alten Speicherblock dorthin kopieren, den alten Block freigeben. Das kostet Zeit und auch Speicher, denn der GC läuft nur, wenn es nötig ist. Unnötig ist es, wenn (a) das System noch genug Speicher hat und (b) der Browser eigentlich gerade was anderes tut. Und das tut er, denn du rechnest ja fleißig rum. Das laufende Programm anhalten und den Speicher reorganisieren ist etwas, was eine GC-Sprache nur im Notfall tut.

In .net hätte ich GC.collect(). Da ist es nötig, weil man mit .net auch Serversysteme bauen kann, die nahe an Realtime sind. In JavaScript kannst Du die Collection nicht erzwingen.

Du weißt, wie groß dein Zielarray ist, und kannst den nötigen Speicher gleich zu Beginn bestellen. Mit

const newArray = Array(array.length);
for (let i=0; i<array.length; i++) {
   newArray[i] = array[i]*2;
}

halbierst Du locker die Laufzeit. Zumindest gerade in meinem Versuch mit 100000 Einträgen - von 19ms auf 8ms.

Du hast aber immer noch eine Map, die hypothetisch jeden Typ aufnehmen kann. Entsprechend muss JS dafür Speicher reservieren und kann den Code auch nicht wirklich optimieren. Du könntest auf ein TypedArray zurückgreifen. Das setzt aber voraus, dass Du deinen Wertebereich kennst. Reicht ein 16-bit Integer, mit oder ohne Vorzeichen? Es gibt Int8Array, Int16Array, Int32Array, Uint8Array, Uint16Array, Uint32Array, Float32Array und Float64Array. BigInt64Array ist etwas umständlich, weil es nur BigInt-Werte verarbeitet und die muss man immer manuell konvertieren.

Mit new UInt32Array(1000) bekommst Du einen ArrayBuffer mit 4000 Bytes, und die 1000 UInt32-Werte liegen darin dicht an dicht. Das schafft ein normales JavaScript Array nicht. Die Zugriffe darauf sind etwas schneller, nicht viel, aber auf jeden Fall ist der Speicher und ggf. auch der Transport zwischen Webworkern effizienter.

Was nichts bringt, ist a.length in einer Variablen zwischenzuspeichern. Sowas musst man in den 80ern tun, da gab's auch den Trick, bei array.length-1 zu beginnen und rückwärts zu zählen, weil ein Test auf i>=0 effizienter war als i<array.length. Ein besonderer Held meinte mal zu mir, ich sollte nicht >= oder <= verwenden, weil das ja zwei Tests und nicht nur einer wären. Gut, dass ich Assembler kannte und wusste, dass der Prozessor für < und <= eigene Instruktionen hat…

Die Grundregel ist eigentlich immer: Versuche, das Anlegen von Objekten zu vermeiden. Wenn Du Speicher allocierst, dann in einer bekannten Größe. Größenänderungen bedeuten Umkopieren.

Was relativ billig ist, ist das schnelle Anlegen und wieder Verwerfen kleiner Temp-Objekte. Ein Mark-and-Sweep Collector wie in GC ist für diese Art der Speichernutzung optimiert. Aber ein Objekt, das länger verwendet wird und dann auch noch ständig umkopiert werden muss, wie Dein newArray, ist ein ziemlicher Sparren im Speichergetriebe.

Im Detail hilft nur: Messen und Probieren. Mit performance.now() bekommst Du einen ziemlich präzisen Timestamp als Bezugspunkt (der nur leider von den Browsern künstlich abgestumpft wird, weil man damit zu genau messen konnte, was der PC tut und das zum Hacken genutzt hat). Man kann auf 5µs Genauigkeit kommen, dazu steht bei MDN was.

Rolf

--
sumpsi - posui - obstruxi