2013-09-30 33 views
8

Ho una matrice allocata dinamicamente di numeri interi in cui voglio inserire numeri interi in posizioni arbitrarie. Molti numeri interi come in più di 2,5 milioni.Come gestisco in modo efficiente più inserti in un array?

Il mio codice attualmente appare così:

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; 

(Il codice reale è un metodo di una classe che ha FSortedList e FCOUNT come campi.)

Utilizzando un elenco temporaneo e l'utilizzo di Sposta piuttosto che un ciclo for per lo spostamento dei dati ha migliorato le prestazioni già abbastanza perché impedisce che l'array venga copiato due volte quando deve crescere (una volta in SetLength sull'array esistente e un'altra volta con Move).

Ma il caso peggiore Insert (SomeValue, 0) sposta sempre tutti i valori esistenti.

Finora stavo pensando lungo la linea di introdurre un offset all'inizio dell'array quindi piuttosto che dover spostare tutti i valori esistenti ogni volta che un nuovo valore è inserito nella parte anteriore, potrei farlo solo quando l'offset raggiunge 0. Es:

// 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; 

(Questo codice è testato e probabilmente buggy) Ciò naturalmente può essere esteso per verificare se il punto di inserimento è più vicino all'inizio o alla fine e seconda che si muovono sia il primo o gli ultimi valori di una posizione in modo tale che in media solo 1/4 delle voci deve essere spostato anziché 1/2 come è attualmente.

Un'altra opzione sarebbe l'implementazione di un array sparse. Ricordo di aver visto una tale implementazione in alcune biblioteche commerciali negli anni '90, ma non ricordo quale fosse (TurboPower?).

Questa procedura è fondamentale per alcuni codici di ordinamento e indicizzazione che funzionano su matrici di dimensioni diverse, da poche decine di voci fino ai milioni di voci sopra menzionate.

Attualmente il programma è in esecuzione per circa 2 ore (prima delle mie ottimizzazioni erano prossime alle 5 ore) e so già che il numero di voci nell'array è almeno raddoppiato. Man mano che le prestazioni degli inserti peggiorano, tanto più grande è già l'array, sospetto che con il doppio del numero di voci, il tempo di esecuzione sarà almeno quadruplo.

Vorrei alcuni suggerimenti su come ottimizzare le prestazioni. Il consumo di memoria non è attualmente un grosso problema, ma il tempo di esecuzione lo è sicuramente.

(Questo è Delphi 2007, ma che non dovrebbe fare molta differenza a meno che le nuove versioni di Delphi hanno già una libreria ottimizzata per fare quanto sopra Classes.TList non è ottimizzato..)

Edit1: Appena trovato la scarsa implementazione dell'array che ho menzionato sopra: è StColl di TurboPower SysTools.

Edit2: Ok, un po 'di background: Il mio programma legge una tabella DBase con attualmente 2,4 milioni di voci e genera diverse nuove tabelle da queste voci. Le nuove tabelle sono normalizzate e sono indicizzate dopo che sono state create (per ragioni di prestazioni non generi gli indici prima di inserire i dati, credetemi, l'ho provato prima.). La matrice è la parte centrale del codice che fornisce l'ordinamento interno per le tabelle generate. I nuovi record vengono aggiunti alla tabella, ma il loro RecNo viene inserito nell'array in ordine ordinato.

+2

Vedere ['Implementazione migliorata di array di fette] (http://www.cromis.net/blog/2013/03/improved-sliced-array-implementation/) di [' @ Runner'] (http: // stackoverflow.com/users/118765/runner) se questo fornisce un input su come migliorare l'ordinamento. –

+0

@LURD: Grazie. Ho letto questo post sul blog quando lo ha scritto (il primo commento su quella pagina è mio) ma avevo già dimenticato. – dummzeuch

+0

Dicci di più su casi d'uso per questo "array inseribile". Possibili soluzioni dipendono da loro. – MBo

risposta

1

non essere un guastafeste, ma la soluzione è già in fase di montaggio alla mia domanda:

Dopo il passaggio da un array per TurboPower's StColl le prestazioni non sono più degrada con matrici di grandi dimensioni ed è abbastanza veloce per l'avvio. Il tempo di esecuzione è inferiore da 2 ore a meno di 1/2 ora. Il cambiamento è stato davvero semplice. Vorrei aver ricordato quella libreria molto prima.

avevo bisogno i seguenti file dal repository SourceForge (non ho voglia di scaricare l'intera libreria):

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

In realtà io sono s urpreso che non c'erano più interdipendenze. I ragazzi di TurboPower sicuramente conoscevano il loro mestiere. Mi chiedo cosa stiano facendo oggi, continuando a programmare macchine da gioco per i casinò?

+1

Se la matrice sparsa è la risposta, la domanda è errata. Array e array sparsi sono piuttosto diversi. Se si dispone di un array sparse, si assegna l'intero array in primo piano e non si effettuano mai le mosse. Nel Delphi moderno dovresti usare 'TDictionary '. –

+0

OK, hai ragione, quello di cui avevo bisogno era una matrice come la struttura dati per i numeri interi che mi permettesse di inserire in modo efficiente nuove voci in qualsiasi posizione casuale. TStCollection fornisce questo. Non ho bisogno della possibilità che TStCollection abbia spazi vuoti inutilizzati per questa applicazione. – dummzeuch

+0

@DavidHeffernan Il commento (TDictionary utilizzato come array sparse per l'aumento delle prestazioni) è ben informato! – SOUser

3

Immediatamente dopo aver osservato la procedura ho notato alcuni difetti. Per vedere i progressi ho prima misurato la velocità della procedura esistente nella peggiore delle ipotesi (aggiungendo il numero sempre nella posizione 0).

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

di misura: n = 500000 47,6 ms

A) Semplicità

ho cancellato alcune righe inutili dal procedimento (OldList è del tutto inutile, SetLength conserva memoria).

Miglioramento 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; 

guadagnare velocità 6% (44,8 ms)

B) Tutto conta

if Length(FSortedList) < FCount + 1 
    then SetLength(FSortedList, FCount + 100); 
  • Suggerimento 1: funzione di lunghezza io i denominato su ogni inserto
  • Tip 2: FCOUNT + 1 viene calcolato ogni volta
  • Tip 3: parametri della procedura come const (di riferimento)
  • Tip 4: introdurre FCapacity variabile
  • Tip 5: lunghezza crescente da solo 100 causeranno molte riallocazioni (25.000 su 2.5 milioni di array). Come dici tu, la memoria non è il problema, quindi perché non solo preallocare tutto o almeno grande?

Miglioramento 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; 

guadagnare velocità 1% (44,3 ms).

Suggerimento: invece di Inc per 100000 è possibile implementare un algoritmo progressivo.

C) Collo di bottiglia

Se guardiamo la procedura ormai, non c'è più niente, solo un sacco di memoria si muove. Se non possiamo modificare l'algoritmo, dobbiamo migliorare lo spostamento della memoria.

c'era effettivamente sfida fastmove (fastcode.sourceforge.net)

ho preparato una zip, con solo quei file necessari (3 file, il codice sorgente). Link >>>http://www.dakte.org/_stackoverflow/files/delphi-fastcode.zip

  • Aggiungi fastcodeCPUID.pas e fastmove.pas al tuo progetto!
  • Inserisci utilizza fastmove.pas;
  • Voilà! Niente altro da cambiare!

guadagnare velocità sulla mia macchina quasi il 50% (dipende dalla CPU in uso).

procedura originale

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

migliorata, senza fastmove (-7%)

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

migliorata Con fastmove (-46%)

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

Ultimi commenti:

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

come ho detto si può aggiungere un po 'aumento FCapacity progressivo. Questa è una classica implementazione di Grow (basta aggiungerne di più se necessario o cambiare 100000 ad un valore più appropriato).

D) Update 2: Array come^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; 

guadagnare velocità dal punto C) Nessuno!

Conslusione: Cambio FastMove o Algoritmo, è stato raggiunto il limite "fisico" della velocità di spostamento della memoria!

Sto usando Delphi XE3 e in Sistema.pas, linea 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); 

Quindi, in realtà in Delphi sono giá alcune routine FASTCODE, ma compresi quelli scaricati direttamente dal loro sito (o da link che ho incluso sopra) ha reso il più grande diferrence, quasi il 50% (strano).

+0

Grazie per la tua ampia risposta. Non capisco, da dove proviene il miglioramento della velocità nel "Miglioramento A". Quello che intendevo fare con la variabile OldList era impedire la copia dell'intero contenuto esistente nella chiamata SetLength (che, come dici tu, conserva il contenuto esistente). Così ho assegnato un nuovo array e copiato solo quella parte del vecchio array da 0 a InsertPos, quindi spostato la parte dopo InsertPos nel nuovo array. Ciò avrebbe dovuto impedire circa la metà del contenuto dell'array da copiare due volte. – dummzeuch

+0

@dummzeuch Quando sono coinvolte molte delle stesse operazioni, 500.000 o anche milioni, ogni chiamata di funzione conta. Il miglioramento della velocità in A è dovuto a: 1. rimossa la variabile locale, non è necessario alcuno stack; <= 3 parametri della funzione vengono passati nei registri della CPU in Delphi, 2. rimossa l'assegnazione alla variabile locale, 3. rimossa deallocazione della memoria con zero, 4. eliminazione dell'allocazione con setlength, 5. chiamata rimossa CopyMemory (sì, anche se si chiama la funzione che non fa nulla richiede tempo prezioso;) È necessario un solo SetLength, che rialloca la memoria, copia i dati e libera i vecchi dati ed è ottimizzato per quello che fa. – david

+0

@dummzeuch (limite di caratteri) Nella procedura originale hai allocato nuova memoria con SetLength 'SetLength (FSortedList, FCount + 100);' SetLength azzera anche tutta la memoria in modo che Totaly sia ridondante per liberare memoria, allocare memoria, cancellare memoria e copiare memoria cancellata. Se si chiama solo SetLength, assegna nuova memoria e copia il contenuto corrente, ma azzera solo il resto dell'array (e anche questo azzeramento è ridondante), quindi è solo una procedura in due fasi, quasi senza ridondanza;) – david

Problemi correlati