Halbdynamisches Array - verbessert die Performance von Größenänderungen erheblich

  • Ausgehend von dieser Diskussion: >>Performanceanalyse der neuen Datenstruktur "Map"<< habe ich mir Gedanken gemacht, wie man die Folgen eines ReDim minimieren kann.

    Arrays verbrauchen in der Regel exakt so viel Speicher, wie Elemente in diesem enthalten sind.
    Da diese Elemente immer direkt hintereinander stehen, führt dies zu Problemen wenn man Elemente hinzufügen möchte.
    Es muss erst ein größerer Speicher reserviert werden, welcher Platz für das neue Array bietet und dann müssen alle Inhalte vom alten in das neue Array kopiert werden.
    Diese Aufgabe übernimmt ReDim. Je größer so ein Array wird umso länger dauert dann jedes ReDim.
    Ein klassisches Array ist also denkbar ungeeignet wenn man Elemente dynamisch hinzufügen möchte.
    Zwar gibt es alternative Datenstrukturen wie ArrayLists. Diese haben zudem den Vorteil ganz fix neue Elemente innerhalb des Arrays einzufügen. Aber der Direktzugriff auf zufällige Elemente ist bei diesen deutlich langsamer.

    Eine Idee die Folgen von andauerndem ReDim zu minimieren ist folgender:
    Wenn ein Element hinzukommt dann wird die Größe des Arrays nicht um 1 erhöht sondern gleich verdoppelt wenn kein Platz mehr im Array vorhanden ist. Damit hat man dann noch Luft für weitere kommende Elemente.
    Auf diesem Weg spart man sich einige ReDim ein. Analog verhält sich das Ganze beim Entfernen von Elementen.

    Folgendes ignorieren - Funktionen wurden zu einer ausgewachsenen UDF ausgebaut: >>DynArray-UDF<<

    Hierzu habe ich mal eine UDF erstellt (liegt im Anhang) welche folgende Funktionen beinhaltet:

    Code
    _DynArrayPush         - neuen Wert ans Ende anhängen
    _DynArrayPop          - letzten Wert zurückgeben und aus dem Array entfernen
    _DynArrayInsert       - einen oder mehrere Werte innerhalb des Arrays einfügen
    _DynArrayDelete       - einen oder mehrere Werte innerhalb des Arrays entfernen
    _DynArraySearch       - Werte in einem unsortierten Array finden
    _DynArrayBinarySearch - Werte in einem sortierten Array finden
    _DynArraySort         - Array sortieren
    _DynArrayMap          - eine beliebige Funktion auf alle Elemente des Arrays anwenden




    Ein Beispiel zur Verwendung wäre folgendes:

    Beispiel zur UDF &quot;DynArray&quot;
    [autoit]

    #include "DynArray.au3"; Neues ArrayGlobal $a_Array[1] = [0]; Dem Array dynamisch Elemente hinzufügenFor $x = 1 To 9 _DynArrayPush($a_Array, "Test " & $x)Next_ArrayDisplay($a_Array, "Das Array"); Einzelne Elemente innerhalb des Arrays einfügen:_DynArrayInsert($a_Array, "Insert 1", 4)_DynArrayInsert($a_Array, "Insert 2", 5); Mehrere Elemente auf einmal einfügen:Local $a_Temp[] = ["Insert 3", "Insert 4", "Insert 5", "Insert 6"]_DynArrayInsert($a_Array, $a_Temp, 6)_ArrayDisplay($a_Array, "Das Array nach _DynArrayInsert"); Element in einem unsortierten Array suchen:MsgBox(0, "_DynArraySearch", 'Wert "Test 5" am Index ' & _DynArraySearch($a_Array, "Test 5") & ' gefunden.'); Array sortieren:_DynArraySort($a_Array)_ArrayDisplay($a_Array, "Das Array nach _DynArraySort"); Element in einem sortierten Array suchen:MsgBox(0, "_DynArrayBinarySearch", 'Wert "Test 5" am Index ' & _DynArrayBinarySearch($a_Array, "Test 5") & ' gefunden.'); Das letzte Element abrufen und aus dem Array entfernen:MsgBox(0,"_DynArrayPop", 'Das letzte Element: "' & _DynArrayPop($a_Array) & '"'); Elemente löschen:_DynArrayDelete($a_Array, 4, 6)_ArrayDisplay($a_Array, "Das Array nach _DynArrayDelete"); Beispiel für ArrayMap$a_Upper = _DynArrayMap($a_Array, StringUpper)_ArrayDisplay($a_Upper, "Das Array nach _DynArrayMap")

    [/autoit]


    Fassen wir mal die Vor-und Nachteile zusammen:

    Vorteile:

    • Deutlich gesteigerte Performance beim Hinzufügen von Elementen.

    Nachteile:

    • Doppelter Speicherverbrauch im Worst-Case
    • Unstetiger Zeitbedarf für jedes Hinzufügen
    • Zwang zur Notation der Elementanzahl im ersten Element

    Was bringt nun aber _DynArrayPush() im Vergleich zu _ArrayAdd() im Endeffekt wirklich?

    Hierzu kann man bei sich einfach mal folgendes Skript ausführen welches den Zeitbedarf beim dynamischen Hinzufügen von 1000 Elementen vergleicht:

    Performancevergleichsskript _DynArrayPush() -- _ArrayAdd()
    [autoit]

    #include "DynArray.au3"Global $N = 1e3$iT = TimerInit()Global $a_Array[1] = [0]For $i = 1 To $N _DynArrayPush($a_Array, "Test " & $i)Next$iT = TimerDiff($iT)ConsoleWrite(StringFormat("% 15s: \t%7.1f ms\n", "_DynArrayPush()", $iT))_ArrayDisplay($a_Array)$iT = TimerInit()Global $a_Array[] = []For $i = 1 To $N _ArrayAdd($a_Array, "Test " & $i)Next$iT = TimerDiff($iT)ConsoleWrite(StringFormat("% 15s: \t%7.1f ms\n", "_ArrayAdd()", $iT))_ArrayDisplay($a_Array)

    [/autoit]


    Meine Ergebnisse hierbei waren folgende:

    Code
    _DynArrayPush(): 	   10.1 ms
        _ArrayAdd(): 	  222.6 ms


    Dieser Zeitvorteil steigt exponentiell mit der Elementanzahl

    4 Mal editiert, zuletzt von AspirinJunkie (22. September 2015 um 14:47)

    • Offizieller Beitrag

    Ich handhabe das ähnlich, allerdings nicht mit Verdopplung der Arraygröße, sondern ich stimme das für die Anwendungen einzeln ab.
    Dabei gehe ich von einer vorerst zu erwartenden realistischen Arrayobergrenze $max aus. 20% davon werden bereitgestellt. Jeweils bei Erreichen dieser Grenze wird das Array um den gleichen Wert erhöht. Bei Erreichen von $max wechsele ich von der statischen Vergrößerung (20% von $max) auf eine dynamische Vergrößerung (20% von UBound), sodass mit wachsender Arraygröße dann auch größere Häppchen angefügt werden.
    Wobei man vllt. auch sagen sollte, dass es hier um Arrays mit wirklichem Inhalt geht, also ich betrachte als Untergrenze 800-1000 Elemente. Was kleiner zu erwarten ist, wird in einem Aufwasch definiert. Ausnahme wäre, wenn ich im Array speicherlastige Inhalte ablege (andere Arrays, Objekte) und allgemein in einer speicherarmen Umgebung bin.
    Wer statistisch veranlagt ist, kann ja mal austesten, wie die unterschiedlichen Herangehensweisen mit der Arraygröße korrelieren.

    Zitat

    Zwang zur Notation der Elementanzahl im ersten Element

    Das läßt sich umgehen, mit einer Statischen Variable in deiner Arrayfunktion. Ich würde ein Static Array einbinden und als zusätzlichen Parameter einen Index, damit die Funktion für mehrere Array gleichzeitig genutzt werden kann. Im Static Array am übergebenen Index den Zähler für das jeweilige zu ändernde Array eintragen.

  • Das läßt sich umgehen, mit einer Statischen Variable in deiner Arrayfunktion. Ich würde ein Static Array einbinden und als zusätzlichen Parameter einen Index, damit die Funktion für mehrere Array gleichzeitig genutzt werden kann. Im Static Array am übergebenen Index den Zähler für das jeweilige zu ändernde Array eintragen.

    Was ist die "Arrayfunktion" bei mir?
    _DynArrayPush? Diese wird ja erst aufgerufen wenn das Array schon erstellt habe.
    Und wenn ich das Array dann nicht mehr benötige und lösche? Vielleicht sogar mit einer Funktion _ArrayDelete(). Wie lösche ich dann dessen Eintrag in der statischen Variable?

    • Offizieller Beitrag

    _DynArrayPush? Diese wird ja erst aufgerufen wenn das Array schon erstellt habe.
    Und wenn ich das Array dann nicht mehr benötige und lösche? Vielleicht sogar mit einer Funktion _ArrayDelete(). Wie lösche ich dann dessen Eintrag in der statischen Variable?

    Die Verwaltung wird mit in die Funktion eingebunden, das erfordert dann, dass das Löschen auch damit vorgenommen wird:

    Spoiler anzeigen
    [autoit]

    Global $ar_1[1]
    Global $ar_2[1]

    [/autoit] [autoit][/autoit] [autoit][/autoit] [autoit]

    For $i = 1 To 20
    ConsoleWrite(_DynArrayPush($ar_1, $i, 1) & @LF)
    If Mod($i, 2) = 0 Then ConsoleWrite(_DynArrayPush($ar_2, $i, 2) & @LF) ; nur jedes 2.te mal ausführen
    ConsoleWrite('Loop_' & $i & ': UBound $ar_1 = ' & UBound($ar_1) & ', UBound $ar_2 = ' & UBound($ar_2) & @LF)
    Next

    [/autoit] [autoit][/autoit] [autoit]

    _DynArrayPush($ar_2, '', -2)
    ConsoleWrite('IsArray( $ar_2 :( ' & IsArray($ar_2) & @LF)

    [/autoit] [autoit][/autoit] [autoit]

    Func _DynArrayPush(ByRef $a_Array, $value, $index)
    If Not IsArray($a_Array) Then Return SetError(1, 0, False)

    [/autoit] [autoit][/autoit] [autoit]

    ; ---- Zählerverwaltung
    Local Static $aCounter[100] ; belegen ab Index 1, [0] bleibt ungenutzt
    If $index < 0 Then
    $aCounter[Abs($index)] = 0 ; negativer Index löscht Einträge
    $a_Array = '' ; evtl. das Array auch gleich leeren ?
    Return
    EndIf
    ; ----

    [/autoit] [autoit][/autoit] [autoit]

    Local $w = UBound($a_Array)
    ;~ $a_Array[0] += 1
    ;~ Local $N = $a_Array[0]
    $aCounter[$index] += 1
    Local $N = $aCounter[$index] -1

    [/autoit] [autoit][/autoit] [autoit]

    If $N > $w Or (Not IsInt($N)) Then Return SetError(2, 0, False)

    [/autoit] [autoit][/autoit] [autoit]

    If $N = $w Then ReDim $a_Array[2 * $w] ; verdopple falls Array-Grenze erreicht wurde
    $a_Array[$N] = $value
    Return True
    EndFunc ;==>_DynArrayPush

    [/autoit]
  • Ok dann hätte man die Push-Funktion.
    Pop geht dann aber nicht mehr da diese ja keinen Zugriff auf $aCounter in _DynArrayPush() hat.

    Sicherlich geht es so aber es wird dann schon ziemlich unintuitiv. (Man muss sich den Index merken, dieser muss sicher vor Doppelbelegung sein, Löschen mit einer Push-Funktion... ;-))
    Aber ja du hast Recht: prinzipiell geht es.

    • Offizieller Beitrag

    Ja, wenn man es wirklich intuitiv führen wollte, müsste man alle Aufrufe für Pop/Push/Delete oder was noch hinzukommt, über einen Wrapper jagen, der dann die richtige Funktion auswählt und entsprechend den Zähler manipuliert. Wobei das noch einfach ist. Eine automatische Erkennung, welches Array denn gerade übergeben wird, kann man ja auch nicht ohne Verrenkungen realisieren. Das erfordert dann irgendeine Form der Registrierung/ID-Vergabe. Stimmt schon - optimal geht anders.. ;)

  • Eine automatische Erkennung, welches Array denn gerade übergeben wird, kann man ja auch nicht ohne Verrenkungen realisieren. Das erfordert dann irgendeine Form der Registrierung/ID-Vergabe. Stimmt schon - optimal geht anders.. ;)

    Und wiedereinmal stoßen wir an diese Grenze. Wie oft sind wir in den letzten 2 Wochen denn hier gelandet ?
    - Funktionen die "wissen" welche Variablen in den Parametern stecken
    - Funktionen die "wissen" von welcher Variable sie aufgerufen wurden
    - Die Unterscheidung mehrer Variablen ohne den Inhalt zu beeinflussen, usw usw.

    Gerade für Arrays gibts doch intern irgendwelche IDs. Man kann ja 2 identische Arrays mal mit "=" vergleichen und wird feststellen dass irgendein Pointer oä verglichen wird und nicht der Inhalt.