2013-09-30 9 views
8

Ich habe ein dynamisch zugeordnetes Array von ganzen Zahlen, in die ich ganze Zahlen an beliebigen Positionen einfügen möchte. Viele ganze Zahlen wie in mehr als 2,5 Millionen.Wie kann ich effizient mehrere Einfügungen in ein Array behandeln?

derzeit Mein Code sieht wie folgt aus:

type 
    TIntegerArr = array of Integer; 

var 
    FCount: Integer; 
    FSortedList: TIntegerArr; 

procedure Insert(_Value: Integer; _InsertPos: integer); 
var 
    OldList: TIntegerArr; 
begin 
    OldList := FSortedList; 
    if Length(FSortedList) < FCount + 1 then begin 
    OldList := FSortedList; 
    FSortedList := nil; 
    SetLength(FSortedList, FCount + 100); 
    CopyMemory(@FSortedList[0], @OldList[0], SizeOf(Integer) * _InsertPos); 
    end; 
    MoveMemory(@FSortedList[_InsertPos + 1], @OldList[_InsertPos], SizeOf(Integer) * (FCount - _InsertPos)); 
    FSortedList[_InsertPos] := _Value; 
    Inc(FCount); 
end; 

(Der eigentliche Code ist eine Methode einer Klasse, den FSortedList und FCount als Felder.) Eher

Mit einer temporären Liste und mit Verschieben als Eine for-Schleife zum Verschieben der Daten hat die Performance schon ziemlich verbessert, weil sie verhindert, dass das Array zweimal kopiert wird, wenn es wachsen muss (einmal in SetLength auf dem bestehenden Array und ein anderes Mal mit Move).

Aber der schlimmste Fall Insert (SomeValue, 0) verschiebt immer noch alle vorhandenen Werte.

Bisher dachte ich über die Einführung eines Offsets am Anfang des Arrays nach, anstatt jedes Mal, wenn ein neuer Wert an der Front eingefügt wird, alle vorhandenen Werte zu verschieben, könnte ich das nur beim Offset tun 0. zum Beispiel erreicht:

// simple case: inserting at Position 0: 
if FOffset = 0 then begin 
    // [...] reallocate a new array as above 
    Move(@FSortedList[100], @OldList, SizeOf(Integer) * _InsertPos); 
    FOffset := 100; 
end; 
Dec(FOffset); 
FSortedList[FOffset] := _NewValue; 

(Dieser Code ist nicht getestet und wahrscheinlich Buggy) Dies natürlich erweitert werden kann, um zu überprüfen, ob der Einfügemarke näher am Anfang oder am Ende und in Abhängigkeit von dieser Bewegung entweder die ersten oder die letzten Werte um eine Position, so dass im Durchschnitt nur 1/4 der Einträge verschoben werden muss und nicht 1/2, wie es derzeit ist.

Eine andere Option wäre die Implementierung eines Sparse-Arrays. Ich erinnere mich, dass ich in den 1990er Jahren eine solche Implementierung in einer kommerziellen Bibliothek gesehen habe, aber ich kann mich nicht erinnern, welche es war (TurboPower?).

Dieses Verfahren ist von zentraler Bedeutung für einige Sortier- und Indexierungscodes, die auf Arrays unterschiedlicher Größe funktionieren, von einigen Dutzend Einträgen bis zu den oben genannten Millionen von Einträgen.

Derzeit läuft das Programm etwa 2 Stunden (vor meinen Optimierungen war es fast 5 Stunden) und ich weiß bereits, dass die Anzahl der Einträge im Array mindestens verdoppelt wird. Da die Insert-Performance immer schlechter wird, je größer das Array bereits ist, vermute ich, dass sich die Laufzeit bei doppelter Anzahl der Einträge mindestens vervierfacht.

Ich möchte einige Vorschläge, wie Sie die Leistung einstellen. Speicherverbrauch ist derzeit kein großes Problem, aber die Laufzeit ist definitiv.

(Dies ist Delphi 2007, aber das sollte nicht viel Unterschied machen, es sei denn neuere Delphi-Versionen bereits eine optimierte Bibliothek für die oben tun Classes.TList nicht optimiert ist..)

Edit1: fand gerade die spärliche Array-Implementierung Ich erwähnte oben: Es ist StColl von TurboPower SysTools.

Edit2: Ok, etwas Hintergrund: Mein Programm liest eine DBase-Tabelle mit aktuell 2,4 Millionen Einträgen und generiert aus diesen Einträgen mehrere neue Tabellen. Die neuen Tabellen sind normalisiert und werden indiziert, nachdem sie erstellt wurden (aus Leistungsgründen erzeuge ich die Indizes vor dem Einfügen der Daten nicht, vertraue mir, ich habe es zuerst versucht). Das Array ist das zentrale Codeelement, das eine interne Sortierung für die generierten Tabellen bereitstellt. Neue Datensätze werden nur an die Tabelle angehängt, aber ihr RecNo wird in sortierter Reihenfolge in das Array eingefügt.

Nach dem Umschalten von einem Array TurboPower's StColl die Leistung nicht verschlechtert länger mit großen Arrays und ist recht schnell zu booten:

+2

See [ 'Verbesserter Geschnittene Array implementation'] (http://www.cromis.net/blog/2013/03/improved-sliced-array-implementation/) von [' @ Runner'] (http: // stackoverflow.com/users/118765/runner) wenn dies einen Input gibt, wie man das Sortieren besser macht. –

+0

@LURD: Danke. Ich habe diesen Blogeintrag gelesen, als er es schrieb (der erste Kommentar auf dieser Seite gehört mir), hatte es aber schon vergessen. – dummzeuch

+0

Erzähl uns mehr über Anwendungsfälle für dieses "einfügbares Array". Mögliche Lösungen hängen davon ab. – MBo

Antwort

1

kein Spielverderber, aber die Lösung ist bereits in der Bearbeitung auf meine Frage zu sein. Die Laufzeit ist von 2 Stunden auf weniger als 1/2 Stunde gesunken. Die Änderung war wirklich einfach. Ich wünschte, ich hätte mich viel früher an diese Bibliothek erinnert.

ich die folgenden Dateien aus dem Repository Source benötigt (ich wollte nicht die ganze Bibliothek zum Download):

  • StBase.pas
  • StColl.pas
  • StConst.pas
  • StList.pas
  • StDefine.inc

Eigentlich bin ich s Ich war überrascht, dass es keine gegenseitigen Abhängigkeiten mehr gab. Die TurboPower-Jungs kannten ihr Handwerk. Ich frage mich, was sie heute machen, immer noch Spielautomaten für Casinos programmieren?

+1

Wenn Sparse Array die Antwort ist, dann ist die Frage falsch. Sparse Array und Array sind ziemlich unterschiedlich. Wenn Sie ein Sparse-Array haben, dann ordnen Sie das gesamte Array im Voraus zu und führen keine Bewegungen aus. Im modernen Delphi würden Sie 'TDictionary ' verwenden. –

+0

OK, Sie haben recht, was ich brauchte, war ein Array wie die Datenstruktur für Ganzzahlen, mit dem ich neue Einträge an beliebiger Stelle effizient einfügen kann. TStCollection bietet dies. Ich brauche nicht die Möglichkeit, dass TStCollection ungenutzte Lücken für diese Anwendung hat. – dummzeuch

+0

@DavidHeffernan Dieser Kommentar (TDictionary als Sparse-Array verwendet für Performance-Schub) ist kenntnisreich! – SOUser

3

Sofort nach dem Betrachten Ihrer Prozedur bemerkte ich einige Fehler. Um den Fortschritt zu sehen, habe ich zuerst die Geschwindigkeit Ihres bestehenden Verfahrens im Worst-Case-Szenario gemessen (Nummer immer auf der 0-Position).

n:=500000; 
for i:=0 to n-1 
do Insert(i, 0); 

Messung: n = 500000 47,6 ms

A) Einfachheit

Ich löschte einige unnötige Linien von der Prozedur (OldList ist völlig unnötig, SetLength Speicher bewahrt).

Verbesserung A:

procedure Insert(_Value: Integer; _InsertPos: integer); 
begin 
if Length(FSortedList) < FCount + 1 
    then SetLength(FSortedList, FCount + 100); 
    Move(FSortedList[_InsertPos], FSortedList[_InsertPos+1], SizeOf(Integer) * (FCount - _InsertPos)); 
    FSortedList[_InsertPos] := _Value; 
    Inc(FCount); 
end; 

Geschwindigkeitsgewinn 6% (44,8 ms)

B) Alles zählt

if Length(FSortedList) < FCount + 1 
    then SetLength(FSortedList, FCount + 100); 
  • Tipp 1: Funktion Länge ich s bei jedem Einsatz genannt
  • Tip 2: FCount + 1 berechnet wird jedesmal
  • Tipp 3: procedure Parameter als const (Referenz)
  • Tip 4: Einführung FCapacity variable
  • Tip 5: Erhöhung der Länge durch nur 100 werden eine Menge Neuzuteilungen verursachen (25.000 bei 2,5 Millionen Arrays). Wie du sagst, ist die Erinnerung nicht das Problem, also warum nicht einfach alle oder zumindest groß vorprogrammieren?

Improvement B:

procedure Insert(const _Value, _InsertPos: integer); 
begin 
if FCount = FCapacity 
    then begin 
    Inc(FCapacity, 100000); 
    SetLength(FSortedList, FCapacity); 
    end; 
Move(FSortedList[_InsertPos], FSortedList[_InsertPos+1], SizeOf(Integer) * (FCount - _InsertPos)); 
FSortedList[_InsertPos] := _Value; 
Inc(FCount); 
end; 

Geschwindigkeitsverstärkung 1% (44,3 ms).

Hinweis: Anstelle von Inc durch 100000 können Sie einen progressiven Algorithmus implementieren.

C) Bottleneck

Wenn wir die Prozedur jetzt schauen, ist nichts mehr übrig, nur eine Menge Speicher bewegt. Wenn wir den Algorithmus nicht ändern können, müssen wir die Speicherbewegung verbessern.

Es war tatsächlich fastmove Herausforderung (fastcode.sourceforge.net)

ich eine Zip vorbereitet, mit nur jene Dateien, die Sie benötigen (3 Dateien, Quellcode). Link >>>http://www.dakte.org/_stackoverflow/files/delphi-fastcode.zip

  • Fügen Sie fastcodeCPUID.pas und fastmove.pas zu Ihrem Projekt hinzu!
  • Einfügen Verwendet fastmove.pas;
  • Voila! Nichts anderes zu ändern!

Geschwindigkeitsverstärkung auf meiner Maschine fast 50% (abhängig von der CPU Sie verwenden).

Original-Verfahren

n   ms graph 
--------------------------------- 
100000 1.8 * 
200000 7.6 *** 
300000 17.0 ******* 
400000 30.1 ************* 
500000 47.6 ******************** 

Verbesserte, ohne fastmove (-7%)

n   ms graph 
--------------------------------- 
100000 1.6 * 
200000 6.9 *** 
300000 15.7 ****** 
400000 28.2 *********** 
500000 44.3 ****************** 

Verbesserte Mit fastmove (-46%)

n   ms graph 
--------------------------------- 
100000 0.8 * 
200000 3.8 ** 
300000 9.0 **** 
400000 16.3 ******* 
500000 25.7 *********** 

Letzte Kommentare:

if FCount = FCapacity 
    then begin 
    if FCapacity<100000 
     then FCapacity:=100000 
     else FCapacity:=FCapacity*2; 
    SetLength(FSortedList, FCapacity); 
    end; 

Wie gesagt Sie einige progressive FCapacity Anstieg hinzufügen können. Dies ist eine klassische Grow-Implementierung (fügen Sie einfach weitere hinzu, falls erforderlich, oder ändern Sie 100000 in einen passenderen Wert).

D) Update 2: Array als^tarray

type 
    PIntegerArr3 = ^TIntegerArr3y; 
    TIntegerArr3y = array[0..1] of Integer; 

var 
FCapacity3, 
FCount3: Integer; 
FSortedList3: PIntegerArr3; 

procedure ResizeArr3(var aCurrentArr: PIntegerArr3; const aNewCapacity: Integer); 
var lNewArr: PIntegerArr3; 

begin 
GetMem(lNewArr, aNewCapacity*SizeOf(Integer)); 

if FCount3>0 // copy data too 
    then begin 
    if aNewCapacity<FCount3 
     then FCount3:=aNewCapacity; // shrink 
    Move(aCurrentArr^[0], lNewArr^[0], FCount3*SizeOf(Integer)); 
    end; 

FreeMem(aCurrentArr, FCapacity3*SizeOf(Integer)); 
FCapacity3:=aNewCapacity; 
aCurrentArr:=lNewArr; 
end; 

procedure FreeArr3; 
begin 
if FCapacity3>0 
    then begin 
    FreeMem(FSortedList3, FCapacity3*SizeOf(Integer)); 
    FSortedList3:=nil; 
    end; 
end; 

procedure Insert3(const _Value, _InsertPos: integer); 
begin 
if FCount3 = FCapacity3 
    then ResizeArr3(FSortedList3, FCapacity3 + 100000); 
Move(FSortedList3^[_InsertPos], FSortedList3^[_InsertPos+1], SizeOf(Integer) * (FCount3 - _InsertPos)); 
FSortedList3^[_InsertPos] := _Value; 
Inc(FCount3); 
end; 

Geschwindigkeitsgewinn aus Schritt C) Keine!

Konslusion: FastMove oder Algorithmus ändern, "physikalische" Grenze der Speicherbewegungsgeschwindigkeit ist erreicht!

Ich benutze Delphi XE3 und in System.pas, Linie 5307:

(* ***** BEGIN LICENSE BLOCK ***** 
* 
* The assembly function Move is licensed under the CodeGear license terms. 
* 
* The initial developer of the original code is Fastcode 
* 
* Portions created by the initial developer are Copyright (C) 2002-2004 
* the initial developer. All Rights Reserved. 
* 
* Contributor(s): John O'Harrow 
* 
* ***** END LICENSE BLOCK ***** *) 

procedure Move(const Source; var Dest; Count: NativeInt); 

Also eigentlich in Delphi sind allready einige Fastcode Routinen, sondern direkt von ihrer Website heruntergeladen einschließlich (oder von Link i oben enthalten) machte den größten diferrence, fast 50% (seltsam).

+0

Danke für Ihre ausführliche Antwort. Ich verstehe nicht, wo die Geschwindigkeitsverbesserung in "Improvement A" herkommt. Was ich mit der OldList-Variable machen wollte, war das Kopieren des gesamten vorhandenen Inhalts im SetLength-Aufruf (der, wie Sie sagen, vorhandenen Inhalt erhält) zu verhindern. Also habe ich ein neues Array zugewiesen und nur den Teil des alten Arrays von 0 bis InsertPos kopiert und dann den Teil nach InsertPos in das neue Array verschoben. Dies hätte verhindern sollen, dass etwa die Hälfte des Array-Inhalts zweimal kopiert wurde. – dummzeuch

+0

@dummzeuch Bei vielen gleichen Operationen, 500.000 oder gar Millionen, zählt jeder Funktionsaufruf. Geschwindigkeitsverbesserung in A ist, wegen: 1. entfernte lokale Variable, kein Stapel wird benötigt; <= 3 Funktionsparameter werden in CPU-Register in Delphi übergeben, 2. entfernt Zuordnung zu lokalen Variablen, 3. entfernt Freigabe des Speichers mit Null, 4. entfernt Zuordnung mit setlength, 5. entfernt Copymemory Anruf (ja, auch wenn Sie Funktion aufrufen, die braucht nichts kostbare Zeit;) Es wird nur eine SetLength benötigt, die Speicher repliziert, Daten kopiert und alte Daten freigibt und für das was sie tut optimiert ist. – david

+0

@dummzeuch (Zeichen) In Ihrem ursprünglichen Verfahren Sie neue Speicher mit SetLength 'SetLength (FSortedList, FCount + 100) zugeordnet sind;' SetLength Nullen auch alle Speicher, so dass es Totaly redundant freier Speicher ist, weisen Speicher, Speicher löschen und kopieren Sie Speicher gelöscht. Wenn Sie nur SetLength aufrufen, ordnet es neue Speicher und kopiert aktuellen Inhalt, aber Nullen nur die von Array verbleibenden (und sogar diese Nullstellen überflüssig ist), so dass es nur ein zweistufiges Verfahren ist, fast ohne Redundanz;) – david