文字数単位の処理はやってはいけない・実証編

ytqwerty2008-03-31


グラフにしてみました。

インデックス単位アクセス
最初に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つとは比較にならないのはわかっていただけると思います。

*1:サロゲートの処理はしてないので気分的にはUTF-32にしたと思ってください