私のプログラムでは、「|」などの特殊文字を含む何百万もの文字列を処理しています。各文字列内でトークンを分離します。n番目のトークンを返す関数があり、これは次のとおりです。
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 }
この関数は、Delphi 4 を使用していたときに開発しました。この関数は、もともと Fastcode によって開発され、現在 Delphi の StrUtils ライブラリに含まれている非常に効率的な PosEx ルーチンを呼び出します。
最近 Delphi 2009 にアップグレードしましたが、文字列はすべて Unicode です。この GetTok 関数は引き続き機能し、問題なく機能します。
私は Delphi 2009 の新しいライブラリを調べましたが、多くの新しい機能と追加機能があります。
しかし、さまざまな fastcode プロジェクトで、新しい Delphi ライブラリのいずれかで必要な GetToken 関数を見たことがありません。また、 Zarko Gajic の Delphi Split / Tokenizer Functions以外の Google 検索では何も見つかりません。私がすでに持っているものと同じくらい最適化されています。
私のプログラムでは、10%でも改善が見られます。代替手段は StringLists であり、常にトークンを別々に保つことは知っていますが、これにはメモリに関して大きなオーバーヘッドがあり、変換するためにすべての作業を行ったかどうかはわかりません。
うわー。この長々とした話の後で、私の質問は次のとおりです。
GetToken ルーチンの非常に高速な実装を知っていますか? アセンブラに最適化されたバージョンが理想的でしょうか?
そうでない場合、上記の私のコードに改善をもたらす可能性のある最適化はありますか?
フォローアップ: Barry Kelly は、私が 1 年前にファイル内の行の解析を最適化することについて尋ねた質問に言及しました。その時、私は自分の GetTok ルーチンが読み取りや解析に使用されていないことさえ考えていませんでした。GetTok ルーチンのオーバーヘッドを見て、この質問をするようになったのは今だけです。Carl Smotricz と Barry の回答があるまで、私はこの 2 つを結びつけることを考えたことはありませんでした。とても明白ですが、登録されませんでした。ご指摘ありがとうございます。
はい、私の Delim は 1 人のキャラクターなので、大幅な最適化を行うことができます。GetTok ルーチン (上記) で Pos と PosEx を使用すると、次のようなコードを使用して、代わりに文字ごとの検索を使用して高速化できるという考えに目がくらみました。
while (cp^ > #0) and (cp^ <= Delim) do
Inc(cp);
みんなの回答を見て、さまざまな提案を試して比較します。それでは結果を掲載します。
混乱:わかりました、今、私は本当に困惑しています.
Carl と Barry の推奨に従って PChars を使用しました。これが私の実装です。
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 }
紙の上では、これ以上のことはできないと思います。
そこで、両方のルーチンをタスクに配置し、AQTime を使用して何が起こっているかを確認しました。私が行った実行には、GetTok への 1,108,514 回の呼び出しが含まれていました。
AQTime は元のルーチンの時間を 0.40 秒に設定しました。Pos への 100 万回の呼び出しには 0.10 秒かかりました。50 万の TokenNum = 1 コピーに 0.10 秒かかりました。600,000 回の PosEx 呼び出しにかかった時間は、わずか 0.03 秒でした。
次に、同じ実行とまったく同じ呼び出しについて、AQTime を使用して新しいルーチンの時間を計りました。AQTime によると、私の新しい「高速」ルーチンは 3.65 秒かかり、これは 9 倍の長さです。AQTime によると、犯人は最初のループでした。
while (PLine^ <> #0) and (PLine^ <> Delim) do
inc(PLine);
1800 万回実行された while 行は、2.66 秒で報告されました。1600 万回実行された inc ラインは、0.47 秒かかると言われています。
ここで何が起きているのか、今なら分かると思った。昨年提出した質問で、AQTime で同様の問題が発生しました。なぜ CharInSet は Case ステートメントよりも高速ですか?
ここでも Barry Kelly が私にヒントを与えてくれました。これらの数値で明確に示されている結果を圧倒する可能性のある各行にオーバーヘッドが追加されます。新しい「最適化されたコード」で実行された 3,400 万行は、元のコードの数百万行を圧倒し、Pos および PosEx ルーチンによるオーバーヘッドは明らかにほとんどまたはまったくありません。
Barry は、QueryPerformanceCounter を使用して、彼が正しいことを確認するコードのサンプルをくれました。
さて、今度は QueryPerformanceCounter で同じことを行い、私の新しいルーチンが高速であり、AQTime が言うように 9 倍遅くないことを証明しましょう。だからここに行く:
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;
したがって、これは GetTok への 1,000,000 回の呼び出しをテストします。
Pos および PosEx 呼び出しを使用した私の古い手順では、0.29 秒かかりました。PChars を使用した新しいものは 2.07 秒かかりました。
今、私は完全に混乱しています!PChar プロシージャが遅いだけでなく、8 ~ 9 倍も遅い理由を誰か教えてください。
謎解き!Andreas は、Delim パラメータを文字列から Char に変更するように彼の回答で述べました。私は常に Char だけを使用するので、少なくとも私の実装ではこれは非常に可能です。何が起こったのか私は驚いた。
100 万回の呼び出しにかかる時間は、1.88 秒から 0.22 秒に短縮されました。
そして驚いたことに、元の Pos/PosEx ルーチンの時間は、Delim パラメータを Char に変更すると、0.29 秒から 0.44 秒に短縮されました。
率直に言って、Delphi のオプティマイザにはがっかりしています。その Delim は定数パラメーターです。オプティマイザーは、同じ変換がループ内で発生していることに気付き、それを移動して、一度だけ実行されるようにする必要があります。
コード生成パラメーターを再確認します。はい、最適化はTrueで文字列形式はオフにチェックしています。
要するに、Andrea の修正を加えた新しい PChar ルーチンは、私のオリジナルよりも約 25% 高速です (.22 対 .29)。
ここで他のコメントをフォローアップして、テストしたいと思います。
最適化をオフにして文字列形式のチェックをオンにすると、時間が 0.22 から 0.30 に増加するだけです。オリジナルにほぼ同じものを追加します。
アセンブラ コードを使用すること、または Pos や PosEx などのアセンブラで記述されたルーチンを呼び出すことの利点は、設定したコード生成オプションの影響を受けないことです。それらは常に同じ方法で実行され、事前に最適化され、肥大化することはありません。
ここ数日、マイクロ最適化のコードを比較する最良の方法は、CPU ウィンドウでアセンブラー コードを見て比較することだと再確認しました。Embarcadero がそのウィンドウをもう少し使いやすくして、クリップボードに部分的にコピーしたり、その一部を印刷したりできるようにしてくれたらいいのにと思います。
また、この投稿の前半で AQTime を不当に非難しました。新しいルーチンに余分な時間が追加されたのは、AQTime によって追加されたインストルメンテーションが原因だと考えていました。戻って String の代わりに Char パラメーターを使用して確認したところ、while ループは (2.66 から) 0.30 秒に短縮され、inc ラインは (0.47 から) 0.14 秒に短縮されました。インクラインも下がるのは奇妙です。しかし、私はすでにこのすべてのテストに疲れています。
文字単位でループするという Carl のアイデアを取り入れて、そのコードをそのアイデアで書き直しました。.22 秒から .19 秒に短縮されました。したがって、これまでで最高のものがここにあります:
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;
これには、CurToken = Tokennum の比較など、いくつかの小さな最適化がまだある可能性があります。これは、Integer または Byte のいずれか高速な方である必要があります。
しかし、私は今幸せだとしましょう。
StackOverflow Delphi コミュニティに改めて感謝します。