Nel mio programma, elaboro milioni di stringhe che hanno un carattere speciale, ad es. "|" per separare i token all'interno di ciascuna stringa. Ho una funzione per restituire il token n'th, e questo è esso:Esiste una routine veloce GetToken per Delphi?
function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
var
I, P, P2: integer;
begin
P2 := Pos(Delim, Line);
if TokenNum = 1 then begin
if P2 = 0 then
Result := Line
else
Result := copy(Line, 1, P2-1);
end
else begin
P := 0; { To prevent warnings }
for I := 2 to TokenNum do begin
P := P2;
if P = 0 then break;
P2 := PosEx(Delim, Line, P+1);
end;
if P = 0 then
Result := ''
else if P2 = 0 then
Result := copy(Line, P+1, MaxInt)
else
Result := copy(Line, P+1, P2-P-1);
end;
end; { GetTok }
ho sviluppato questa funzione indietro quando stavo usando Delphi 4. Si chiama la routine PosEx molto efficiente che è stato originariamente sviluppato da FASTCODE e è ora incluso nella libreria StrUtils di Delphi.
Recentemente ho aggiornato a Delphi 2009 e le mie stringhe sono tutte Unicode. Questa funzione GetTok funziona ancora e funziona ancora bene.
Ho passato le nuove librerie in Delphi 2009 e ci sono molte nuove funzioni e aggiunte ad esso.
Ma non ho visto una funzione GetToken come ho bisogno in nessuna delle nuove librerie Delphi, nei vari progetti fastcode, e non riesco a trovare nulla con una ricerca su Google diversa da Zarko Gajic's: Delphi Split/Tokenizer Functions, che non è così ottimizzata come quello che ho già.
Qualsiasi miglioramento, anche il 10% sarebbe evidente nel mio programma. So che un'alternativa è StringList e tenere sempre separati i token, ma questo ha un grande overhead di memoria e non sono sicuro di aver fatto tutto il possibile per convertire se sarebbe stato più veloce.
Whew. Quindi, dopo tutto questo lungo colloquio, la mia domanda è davvero:
Sai qualche implementazione molto veloce di una routine GetToken? Una versione ottimizzata per assemblatori sarebbe l'ideale?
In caso contrario, ci sono ottimizzazioni che è possibile vedere nel mio codice sopra che potrebbero migliorare?
ollowup: Barry Kelly citato una domanda ho chiesto un anno fa su come ottimizzare l'analisi delle linee in un file. A quel tempo non avevo nemmeno pensato alla mia routine GetTok che non era usata per quella lettura o analisi. Solo ora ho visto il sovraccarico della mia routine GetTok che mi ha portato a fare questa domanda. Fino alle risposte di Carl Smotricz e Barry, non avevo mai pensato di collegare i due. Così ovvio, ma non è stato registrato. Grazie per la segnalazione.
Sì, il mio Delim è un singolo carattere, quindi ovviamente ho qualche ottimizzazione importante che posso fare. Il mio uso di Pos e PosEx nella routine GetTok (sopra) mi ha accecato l'idea che posso farlo più velocemente con un carattere di ricerca di carattere, invece, con i pezzi di codice come:
while (cp^ > #0) and (cp^ <= Delim) do
Inc(cp);
ho intenzione di passare attraverso le risposte di tutti e provare i vari suggerimenti e confrontarli. Quindi posterò i risultati.
Confusione: Ok, ora sono davvero perplesso.
ho preso Carl e Barry di raccomandazione per andare con PChars, e qui è la mia realizzazione:
function GetTok(const Line: string; const Delim: string; const TokenNum: Byte): string;
{ LK Feb 12, 2007 - This function has been optimized as best as possible }
{ LK Nov 7, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; https://stackoverflow.com/questions/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
I: integer;
PLine, PStart: PChar;
begin
PLine := PChar(Line);
PStart := PLine;
inc(PLine);
for I := 1 to TokenNum do begin
while (PLine^ <> #0) and (PLine^ <> Delim) do
inc(PLine);
if I = TokenNum then begin
SetString(Result, PStart, PLine - PStart);
break;
end;
if PLine^ = #0 then begin
Result := '';
break;
end;
inc(PLine);
PStart := PLine;
end;
end; { GetTok }
Sulla carta, non credo che si può fare molto meglio di questo.
Così ho inserito entrambe le routine nell'attività e ho utilizzato AQTime per vedere cosa sta succedendo.Nella corsa che avevo incluso 1,108,514 chiamate a GetTok.
AQTime ha programmato la routine originale a 0,40 secondi. Il milione di chiamate a Pos ha richiesto 0,10 secondi. Mezzo milione di TokenNum = 1 copie ha richiesto 0,10 secondi. Le 600.000 chiamate PosEx hanno richiesto solo 0,03 secondi.
Poi ho programmato la mia nuova routine con AQTime per la stessa esecuzione e esattamente le stesse chiamate. AQTime riporta che la mia nuova routine "veloce" ha richiesto 3.65 secondi, ovvero 9 volte di più. Il colpevole secondo AQTime stato il primo ciclo:
while (PLine^ <> #0) and (PLine^ <> Delim) do
inc(PLine);
La linea mentre, che è stata eseguita 18 milioni di volte, è stato segnalato in 2,66 secondi. Si dice che la linea inc, eseguita 16 milioni di volte, impieghi 0,47 secondi.
Ora pensavo di sapere cosa stava succedendo qui. Ho avuto un problema simile con AQTime in una domanda che ho posto l'anno scorso:. Why is CharInSet faster than Case statement?
Ancora una volta è stato Barry Kelly che mi risparmiandoci in sostanza, un profiler strumentazione come AQTime non necessariamente fare il lavoro per microoptimization. Aggiunge un overhead ad ogni linea che può sommergere i risultati che è mostrato chiaramente in questi numeri. Le 34 milioni di linee eseguite nel mio nuovo "codice ottimizzato" sommergono le diverse milioni di righe del mio codice originale, con un sovraccarico apparentemente piccolo o nullo delle routine Pos e PosEx.
Barry mi ha fornito un esempio di codice utilizzando QueryPerformanceCounter per verificare che fosse corretto, e in tal caso lo era.
Ok, quindi facciamo lo stesso ora con QueryPerformanceCounter per dimostrare che la mia nuova routine è più veloce e non 9 volte più lenta come AQTime dice che lo è. Così qui vado:
function TimeIt(const Title: string): double;
var i: Integer;
start, finish, freq: Int64;
Seconds: double;
begin
QueryPerformanceCounter(start);
for i := 1 to 250000 do
GetTokOld('This is a string|that needs|parsing', '|', 1);
for i := 1 to 250000 do
GetTokOld('This is a string|that needs|parsing', '|', 2);
for i := 1 to 250000 do
GetTokOld('This is a string|that needs|parsing', '|', 3);
for i := 1 to 250000 do
GetTokOld('This is a string|that needs|parsing', '|', 4);
QueryPerformanceCounter(finish);
QueryPerformanceFrequency(freq);
Seconds := (finish - start)/freq;
Result := Seconds;
end;
Quindi questo metterà alla prova 1.000.000 chiamate a GetTok.
La mia vecchia procedura con le chiamate Pos e PosEx ha richiesto 0,29 secondi. Il nuovo con PChars ha impiegato 2,07 secondi.
Ora sono completamente confuso! Qualcuno può dirmi perché la procedura PChar non è solo più lenta, ma è da 8 a 9 volte più lenta !?
Mistero risolto! Andreas ha detto nella sua risposta di cambiare il parametro Delim da una stringa a un Char. Userò sempre solo un Char, quindi almeno per la mia implementazione è molto possibile. Sono rimasto sbalordito da ciò che è successo.
Il tempo per 1 milione di chiamate è diminuito da 1,88 secondi a 0,22 secondi.
E sorprendentemente, il tempo per la mia procedura Pos/Posex originale è passato da 0,29 a 44 secondi quando ho cambiato il parametro Delim in Char.
Francamente, sono deluso dall'ottimizzatore di Delphi. Quel Delim è un parametro costante. L'ottimizzatore dovrebbe aver notato che la stessa conversione sta avvenendo all'interno del ciclo e dovrebbe averla spostata in modo che fosse eseguita una sola volta.
Doppio controllo dei parametri di generazione del codice, sì, ho Ottimizzazione True e Controllo formato stringa disattivato.
La linea di fondo è che la nuova routine PChar con la correzione di Andrea è circa il 25% più veloce del mio originale (0,22 contro 0,29).
Desidero continuare a seguire gli altri commenti qui e testarli.
Disattivare l'ottimizzazione e attivare la verifica del formato stringa aumenta solo il tempo da .22 a .30. Aggiunge all'incirca lo stesso all'originale.
Il vantaggio di utilizzare il codice assembler o le routine di chiamata scritte nell'assembler come Pos o PosEx è che NON sono soggette alle opzioni di generazione del codice impostate. Funzioneranno sempre allo stesso modo, in modo pre-ottimizzato e non gonfiato.
Ho riaffermato negli ultimi due giorni che il modo migliore per confrontare il codice per la microottimizzazione è quello di esaminare e confrontare il codice Assembler nella finestra della CPU. Sarebbe bello se Embarcadero potesse rendere la finestra un po 'più comoda e consentirci di copiare parti negli Appunti o di stamparne delle sezioni.
Inoltre, ingiustamente ho sbattuto AQTime in precedenza in questo post, pensando che il tempo extra aggiunto per la mia nuova routine era esclusivamente a causa della strumentazione aggiunta. Ora che torno indietro e controllo il parametro Char invece di String, il ciclo while è inferiore a .30 secondi (da 2.66) e la linea inc è inferiore a .14 secondi (da .47). Strano che anche la linea inc entrerebbe. Ma mi sto già stancando da tutti questi test.
Ho preso l'idea di Carl di eseguire il ciclo dei caratteri e ho riscritto quel codice con quell'idea. Fa un altro miglioramento, fino a .19 secondi da .22. Così qui è ora il migliore finora:
function GetTok(const Line: string; const Delim: Char; const TokenNum: Byte): string;
{ LK Nov 8, 2009 - Reoptimized using PChars instead of calls to Pos and PosEx }
{ See; https://stackoverflow.com/questions/1694001/is-there-a-fast-gettoken-routine-for-delphi }
var
I, CurToken: Integer;
PLine, PStart: PChar;
begin
CurToken := 1;
PLine := PChar(Line);
PStart := PLine;
for I := 1 to length(Line) do begin
if PLine^ = Delim then begin
if CurToken = TokenNum then
break
else begin
CurToken := CurToken + 1;
inc(PLine);
PStart := PLine;
end;
end
else
inc(PLine);
end;
if CurToken = TokenNum then
SetString(Result, PStart, PLine - PStart)
else
Result := '';
end;
Ci può essere ancora alcune ottimizzazioni minori a questo, come ad esempio il confronto CurToken = Tokennum, che dovrebbe essere dello stesso tipo, Integer o byte, a seconda di quale è più veloce.
Ma diciamo, sono felice ora.
Grazie ancora alla comunità Delphi di StackOverflow.
milioni Perché l'elaborazione delle stringhe? Forse il tuo programma può essere ottimizzato in modo che non debba farlo. –
Questa domanda è uno dei miei tentativi di ottimizzazione. Quando hai un programma che elabora un file da 300 MB, dovrà fare molto lavoro, ottimizzazioni e trucchi, non importa quale. Ma se il file di input arriva al mio programma così grande, non c'è modo di renderlo più piccolo senza prima elaborarlo. – lkessler
Grazie per il link 1.01pm (un'interessante discussione), ma sono sicuro che la community di StackOverflow apprezzerebbe se la roba off topic fosse stata trasmessa in un altro modo, ad es. come commento sul mio blog. – lkessler