Dans mon programme, je traite des millions de chaînes qui ont un caractère spécial, par ex. "|" séparer les jetons dans chaque chaîne. J'ai une fonction de retourner le jeton n'th, et voici:Existe-t-il une routine GetToken rapide pour 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 }
J'ai développé cette fonction quand j'utilisais Delphi 4. Il appelle la routine de PosEx très efficace qui a été initialement développé par Fastcode et est maintenant inclus dans la bibliothèque StrUtils de Delphi.
J'ai récemment mis à jour vers Delphi 2009 et mes chaînes sont toutes Unicode. Cette fonction GetTok fonctionne toujours et fonctionne toujours bien.
J'ai parcouru les nouvelles bibliothèques dans Delphi 2009 et il y a beaucoup de nouvelles fonctions et ajouts.
Mais je n'ai pas vu une fonction GetToken comme j'ai besoin dans l'une des nouvelles bibliothèques Delphi, dans les différents projets fastcode, et je ne trouve rien avec une recherche Google autre que Zarko Gajic's: Delphi Split/Tokenizer Functions, qui n'est pas aussi optimisée que ce que j'ai déjà.
Toute amélioration, même 10% serait perceptible dans mon programme. Je sais qu'une alternative est StringLists et de garder toujours les jetons séparés, mais cela a une grande surcharge de mémoire et je ne suis pas sûr si j'ai fait tout ce travail pour convertir si ce serait plus rapide.
Ouf. Donc, après toute cette longue discussion, ma question est vraiment:
Connaissez-vous des implémentations très rapides d'une routine GetToken? Une version optimisée d'assembleur serait idéale?
Sinon, y a-t-il des optimisations que vous pouvez voir dans mon code ci-dessus qui pourraient faire une amélioration? Suivi: Barry Kelly a mentionné une question que j'ai posée il y a un an sur l'optimisation de l'analyse des lignes dans un fichier. À ce moment-là, je n'avais même pas pensé à ma routine GetTok qui n'était pas utilisée pour la lecture ou l'analyse syntaxique. Ce n'est que maintenant que j'ai vu le transparent de ma routine GetTok qui m'a conduit à poser cette question. Jusqu'à la réponse de Carl Smotricz et Barry, je n'avais jamais pensé à relier les deux. Tellement évident, mais ça ne s'est pas enregistré. Merci d'avoir fait remarquer cela.
Oui, mon Delim est un seul caractère, donc évidemment j'ai une optimisation majeure que je peux faire. Mon utilisation de Pos et PosEx dans la routine GetTok (ci-dessus) m'a aveuglé à l'idée que je peux le faire plus rapidement avec un caractère par la recherche de caractère au lieu, avec des bouts de code comme:
while (cp^ > #0) and (cp^ <= Delim) do
Inc(cp);
Je vais parcourir les réponses de chacun et essayer les différentes suggestions et les comparer. Ensuite, je posterai les résultats.
Confusion: Bon, maintenant je suis vraiment perplexe.
Je pris la recommandation de Carl et Barry pour aller avec PChars, et voici ma mise en œuvre:
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 }
Sur le papier, je ne pense pas que vous pouvez faire beaucoup mieux que cela. J'ai donc mis les deux routines à la tâche et utilisé AQTime pour voir ce qui se passe.La course que j'avais inclus 1 108 514 appels à GetTok. AQTime a chronométré la routine d'origine à 0,40 seconde. Le million d'appels à Pos a pris 0.10 secondes. Un demi-million de TokenNum = 1 copies a pris 0,10 secondes. Les 600 000 appels PosEx n'ont pris que 0,03 seconde.
Puis j'ai chronométré ma nouvelle routine avec AQTime pour le même passage et exactement les mêmes appels. AQTime rapporte que ma nouvelle routine "rapide" a pris 3,65 secondes, ce qui est 9 fois plus long. Le coupable selon AQtime a été la première boucle:
while (PLine^ <> #0) and (PLine^ <> Delim) do
inc(PLine);
La ligne de temps, qui a été exécuté 18 millions de fois, a été rapportée à 2.66 secondes. La ligne inc, exécutée 16 millions de fois, aurait pris 0,47 secondes. Maintenant, je pensais que je savais ce qui se passait ici. J'ai eu un problème similaire avec AQTime dans une question que j'ai posée l'année dernière: Why is CharInSet faster than Case statement?
Encore une fois c'était Barry Kelly qui m'a clued dedans. Fondamentalement, un profileur d'instrumentation comme AQTime ne fait pas nécessairement le travail pour microoptimization. Il ajoute un surcoût à chaque ligne qui peut masquer les résultats qui sont clairement indiqués dans ces nombres. Les 34 millions de lignes exécutées dans mon nouveau "code optimisé" submergent les quelques millions de lignes de mon code original, avec apparemment peu ou pas de frais généraux des routines Pos et PosEx. Barry m'a donné un échantillon de code en utilisant QueryPerformanceCounter pour vérifier qu'il avait raison, et dans ce cas il l'était. Bon, alors faisons la même chose maintenant avec QueryPerformanceCounter pour prouver que ma nouvelle routine est plus rapide et pas 9 fois plus lente comme le dit AQTime. Alors voilà:
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;
Cela testera 1 000 000 appels à GetTok.
Mon ancienne procédure avec les appels Pos et PosEx a pris 0,29 secondes. Le nouveau avec PChars a pris 2,07 secondes.
Maintenant, je suis complètement déconcerté! Quelqu'un peut-il me dire pourquoi la procédure PChar est non seulement plus lente, mais est 8 à 9 fois plus lent?
Mystery résolu! Andreas a dit dans sa réponse de changer le paramètre Delim d'une chaîne à un Char. Je vais toujours utiliser juste un Char, donc au moins pour ma mise en œuvre c'est très possible. J'ai été étonné de ce qui est arrivé.
La durée du 1 million d'appels est passée de 1,88 secondes à 0,22 seconde.
Et étonnamment, le temps pour ma routine Pos/PosEx d'origine est passé de .29 à .44 secondes lorsque j'ai changé son paramètre Delim à un Char. Franchement, je suis déçu par l'optimiseur de Delphi. Ce Delim est un paramètre constant. L'optimiseur aurait dû remarquer que la même conversion se produisait dans la boucle et devrait l'avoir déplacée afin qu'elle ne soit effectuée qu'une seule fois.
Double vérification de mes paramètres de génération de code, oui j'ai Optimisation True et vérification de format chaîne désactivée.
La ligne de fond est que la nouvelle routine PChar avec la réparation d'Andrea est environ 25% plus rapide que mon original (.22 contre .29).
Je veux toujours faire un suivi sur les autres commentaires ici et les tester. La désactivation de l'optimisation et l'activation de la vérification de format de chaîne ne font qu'accroître la durée de .22 à .30. Il ajoute à peu près la même chose à l'original. L'avantage d'utiliser du code assembleur ou des routines d'appel écrites en assembleur comme Pos ou PosEx est qu'elles ne sont PAS sujettes aux options de génération de code que vous avez définies. Ils fonctionneront toujours de la même manière, de manière pré-optimisée et non-ballonnée.
J'ai réaffirmé ces derniers jours que la meilleure façon de comparer le code pour la microoptimisation est de regarder et de comparer le code assembleur dans la fenêtre CPU. Ce serait bien si Embarcadero pouvait rendre cette fenêtre un peu plus pratique, et nous permettre de copier des parties dans le presse-papiers ou d'en imprimer des sections.
En outre, j'ai injustement critiqué AQTime plus tôt dans ce post, en pensant que le temps supplémentaire ajouté pour ma nouvelle routine était uniquement à cause de l'instrumentation ajoutée. Maintenant que je reviens et que je vérifie avec le paramètre Char au lieu de String, la boucle while est à 0,30 seconde (à partir de 2,66) et la ligne inc à 0,14 seconde (à partir de 0,47). Étrange que la ligne inc descendrait aussi bien. Mais je suis déjà épuisé par tous ces tests. J'ai pris l'idée de Carl de boucler par des caractères, et ai réécrit ce code avec cette idée. Il fait une autre amélioration, jusqu'à 0,19 secondes de 0,22. Voici donc maintenant le meilleur jusqu'à présent:
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;
Il peut y avoir encore quelques optimisations mineures à cela, comme la comparaison CurToken = Tokennum, qui devrait être le même type, Entier ou octet, selon le plus rapide.
Mais disons, je suis heureux maintenant.
Merci encore à la communauté StackOverflow Delphi.
Pourquoi vous traitez des millions de chaînes? Peut-être que votre programme peut être optimisé pour qu'il ne soit pas obligé de le faire. –
Cette question est l'une de mes tentatives d'optimisation. Quand vous avez un programme qui traite un fichier de 300 Mo, il devra faire beaucoup de travail, des optimisations et des astuces, peu importe quoi. Mais si le fichier d'entrée arrive dans mon programme, il n'y a pas moyen de le rendre plus petit sans le traiter en premier. – lkessler
Merci pour ce lien 1.01pm (une discussion intéressante), mais je suis sûr que la communauté StackOverflow apprécierait si des choses hors sujet a été transmis d'une autre manière, par exemple. comme un commentaire sur mon blog. – lkessler