文字数単位の処理はやってはいけない・実証編
グラフにしてみました。
緑 | インデックス単位アクセス |
赤 | 最初にWideStringに変換*1 |
青 | 文字単位アクセス |
処理としては、ランダムに生成した文字列からアルファベットの'A'数えているだけです。あんまり複雑なことする気も無かったし手頃な例思いつかなかったので。SJISでやってますが、別にUTF-8でもUTF-16(サロゲートを考慮する場合)でもEUC-JPでもおんなじですね。
緑はまあ、順当な右肩上がりです。
赤は最初だけ時間がかかってますが、一度変換テーブルがメモリキャッシュに乗ったら後は緑の倍ぐらいです。キャッシュ汚染が嫌ですが、まあまあ。
青は……グラフの通り。
緑と赤はO(N)で、青はO(N^2)なんですね。
オーダーの違いを軽く流すのは害でしかないです。だって右端は1000バイトですよ。この記事より余程少ないテキスト量ですよ。極端な例を意図的に選んだなんて思わないでくださいよ。計算オーダーが違うというのはこういうことです。あくまで文字単位の処理をしたいなら、適切なpacked形式かUTF-32を使うべきです。
本当に関わりたくないんですよ!本当ですよ!ツンデレ気取ってるんじゃなくて、本当に関りたくないんだからねっ。
今現在、日本でDelphiを使うために最も必要なスキルは「日本のDelphiユーザーを」スルーするスキルだと思います、先生!
uses Windows, MMSystem; function KLEN(const S: AnsiString): Integer; var I: Integer; begin Result := Length(S); I := 1; while I < Length(S) do begin if IsDBCSLeadByte(Ord(S[I])) then begin Dec(Result); Inc(I, 2); end else begin Inc(I); end; end; end; function KMID(const S: AnsiString; Index: Integer): ShortString; var I, Moji: Integer; begin I := 1; for Moji := 2 to Index do begin if IsDBCSLeadByte(Ord(S[I])) then begin Inc(I, 2); end else begin Inc(I); end; end; if IsDBCSLeadByte(Ord(S[I])) then begin Result[0] := #2; Result[1] := S[I]; Result[2] := S[I + 1]; end else begin Result[0] := #1; Result[1] := S[I]; end end; procedure ByIndex(const Data: AnsiString; out A: Integer); var I: Integer; begin A := 0; I := 1; while I <= Length(Data) do begin if IsDBCSLeadByte(Ord(Data[I])) then begin Inc(I, 2); end else begin if Data[I] = 'A' then Inc(A); Inc(I); end; end; end; procedure ByWide(const Data: AnsiString; out A: Integer); var WideData: WideString; I: Integer; begin WideData := Data; A := 0; for I := 1 to Length(WideData) do begin if WideData[I] = 'A' then Inc(A); end; end; procedure ByMoji(const Data: AnsiString; out A: Integer); var I: Integer; begin A := 0; for I := 1 to KLEN(Data) do begin if KMID(Data, I) = 'A' then Inc(A); end; end; procedure Test(Length: Integer); function RandomString(Length: Integer): AnsiString; var C: AnsiChar; begin Result := ''; while System.Length(Result) < Length do begin C := Chr(Random(255)); Result := Result + C; end end; type T = procedure(const Data: AnsiString; out A: Integer); procedure Handle(P: T; const Name: String; const Data: AnsiString); var Time1, Time2: Int64; A: Integer; begin QueryPerformanceCounter(@Time1); P(Data, A); QueryPerformanceCounter(@Time2); //WriteLn(Name, ':', Time2 - Time1, ' (', A, ')'); Write(Time2 - Time1); end; var Data: AnsiString; begin //WriteLn('Test ', Length); Data := RandomString(Length); Write(Length, ','); Handle(ByIndex, 'ByIndex', Data); Write(','); Handle(ByWide, 'ByWide', Data); Write(','); Handle(ByMoji, 'ByMoji', Data); WriteLn; end; var I: Integer; begin for I := 1 to 10 do Test(I * 100); end.
KMIDは、返値AnsiStringはあまりにも酷いのでメモリアロケートの無いShortStringにしています。それでだいぶ速くなっています。もうちょい最適化できるとは思いますが、しかしそれで仮に10倍速くなっても、それでも他2つとは比較にならないのはわかっていただけると思います。