文字列操作

解決


はな  2004-03-02 21:11:37  No: 7456

環境:Win2k、Delphi6(Personal)

はじめまして。

文字列処理について教えていただきたいことがあります。
電文で送信したデータをダンプ出力する為に、1バイトずつ
切り出してHex変換しています。サイズが小さいデータなら
早いのですが、サイズが大きいデータになるとこの変換処理に
かなりの時間がかかります。

変換処理はrepeat 〜 untilの中で文字列1byte切り出しの
IntToHexで変換し、String型変数に退避しています。
(出力イメージはB'Z等のダンプ出力ツール)

75Kのデータで行ってみたところ、最後まで待つ事が
出来ないくらいの時間です(5分位は待ちました)。

高速に処理する方法等あるのでしょうか?
(.NETにあるStringBuilderみたいなものとか)

ご教授お願い致します。


にしの  2004-03-02 21:31:05  No: 7457

環境にも夜かと思いますが、72.8KBのファイルを1バイトずつ読み込んで16進数にし、出力するものを試してみると、こちらでは300ミリ秒程度で終了しています。
何か別のところに間違いはありませんか?
# カウントに16bit変数を使用しているとか


jok  2004-03-02 22:32:59  No: 7458

pentium200MHz くらいの Win98 で 79KB のファイルで試しましたがあっと
いう間です。

const
  HexStr:string = '0123456789ABCDEF';

function ByteToHexStr(const value:Byte):string;
begin
  SetLength(result,3);
  result[1] := HexStr[value shr 4 +1];
  result[2] := HexStr[value and $F +1];
  result[3] := ' ';
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  s:string;
  ms:TMemoryStream;
  p:PByte;
  i:integer;
begin
  ms := TMemoryStream.Create;
  try
    ms.LoadFromFile('D:\test.jpg');
    p := ms.Memory;
    SetLength(s,ms.Size*3);
    for i := 0 to ms.Size-1 do begin
      Move(PChar(ByteToHexStr(p^))^,s[i*3+1],3);
      Inc(p);
    end;
    RichEdit1.Text := s;
  finally
    ms.Free;
  end;
end;


はな  2004-03-02 22:50:37  No: 7459

にしのさん、jokさん、
早速のご回答ありがとうございます。

質問の記述で間違いがありました。
75Kではなく750Kです。申し訳ありません。

マシンスペックはPentiumM 1.6GHz、メモリ1Gです。


にしの  2004-03-02 22:52:18  No: 7460

>jokさん

ByteToHexStrは、
IntToHex(Integer(p^), 2) + ' '
とできますね。

私の場合、こんな感じです。jokさんと大差ありません。
FileStreamで1バイトずつ取り込んでいるので、こっちの方が遅いかも。
実際にやる場合は、適当な量のバッファを用意して、バッファ単位で処理した方がよいです。TMemoryStreamで読み込む場合、数GBになると問題が出てきそうです。
# スワップメモリが大量発生しそう

var
  i: integer;
  fs: TFileStream;
  buf: byte;
  str: String;
  cnt: Integer;
  st, et: LongInt;
begin
  st := GetTickCount;
  fs := nil;
  str := '';
  cnt := 0;
  try
    fs := TFileStream.Create('C:\test.dat', fmOpenRead);

    while fs.Read(buf, 1) > 0 do
    begin
      str := str + IntToHex(Integer(buf), 2) + ' ';
      Inc(cnt);
      if cnt > 15 then
      begin
        str := str + #13#10;
        cnt := 0;
      end;
    end;
    Editor1.Lines.Text := str;
  finally
    et := GetTickCount;
    if Assigned(fs) then fs.Free;
    ShowMessage(IntToStr(et - st) + ' ms');
  end;
end;


にしの  2004-03-02 22:56:47  No: 7461

上と同じコードで、2.67MBのデータを処理させたところ、10025 msでした。
Pen4の1.7GHz、メモリ384MBです。
コーディングはどうなってます?


jok  2004-03-02 23:20:11  No: 7462

> にしのさん

ByteToHexStr() は  IntToHex() より速いと思ってわざわざ作りました。

> FileStreamで1バイトずつ取り込んでいるので、こっちの方が遅いかも。

確実に遅いでしょう。メモリ+ポインタより速いアクセス方法はありません。

>  TMemoryStreamで読み込む場合、数GBになると問題が出てきそうです。

お題は1Mバイト以下です。1000倍以上のサイズを心配する必要はないのでは?


はな  2004-03-02 23:29:13  No: 7463

はなです。

ダンプ変換のソースを載せてみます。

jokさん、にしのさんがコーディングされたソースを
調べてみて、自分のと比較してみます。

===================================================
function Dump( const s : string ) : String;

var
  dump, msg : string;
  i, len, c, posi : integer;
const
  colmax : integer = 16;
  cr : char = #13;
  lf : char = #10;
begin

  posi := 0;
  len := Length( s );

  dump := '----------------------------------------------------------------------';
  dump := dump + #13#10 + '      ';

  for i := 0 to colmax - 1 do
  begin
    dump := dump + IntToHex( i, 2 ) + ' ';
  end;
  dump := dump + '0123456789ABCDEF';
  dump := dump;

  repeat

  begin

    if len <= ( posi + colmax ) then
      c := len - posi
    else
      c := colmax;

    dump := dump + #13#10;
    dump := dump + IntToHex( posi, 5 ) + ' ';

    msg := '';
    for i := 1 to colmax do
    begin
      if i > c then
      begin
        dump := dump + '   ';
      end
      else
      begin
        dump := dump + IntToHex( Ord( s[posi+i] ), 2 ) + ' ';
        msg := msg + s[posi+i];
      end;
    end;
    msg := StringReplace( msg, #13, '.', [rfReplaceAll]);
    msg := StringReplace( msg, #10, '.', [rfReplaceAll]);
    dump := dump + msg;
    posi := posi + colmax;
  end;

  until len <= posi;

  result := dump;

end;


jok  2004-03-02 23:59:25  No: 7464

これは、バイナリエディタの画面みたいに16進表示とその横に文字列を表示して
いるのですね。

1行ごとに  StringReplace() を2度も実行しているのは非常に無駄です。
1バイトごとに値を変換しているんですから、そのとき値を調べて  #13 の
ときと #10 ときを自力で変換したほうがいいです。

また、

dump := dump + msg;

こんなふうに文字列を継ぎ足していくのは、メモリの再確保・コピーが頻発して
遅い原因になります。ファイルの大きさが分かれば何行になるかあらかじめ計算
できますので、SetLength() で文字列の長さを確保しておき、内容を1バイト
ずつ入れ替えていくかまとめてコピーするようにすると、効率が上がります。


LupinⅢ  URL  2004-03-03 00:21:19  No: 7465

横から失礼します。
こんなのはどうでしょう?

Buttonを1個
StirngGridを2個
OpenDialogを1個配置して下記をButtonのイベントに記述

procedure TForm1.Button1Click(Sender: TObject);
var
   Start:integer;
   FS:TMemoryStream;
   Tmp:WORD;
   Tmp2:WORD;
   X,Y:integer;
   NextSkip:Boolean;
   PrevStr:string;
begin
   if OpenDialog1.Execute then begin

      StringGrid1.Visible := False;
      StringGrid2.Visible := False;

      FS := TMemoryStream.Create;
      FS.LoadFromFile(OpenDialog1.FileName);
      try
          X := 0;
          Y := 1;
          NextSkip := False;

          StringGrid1.Cells[0,0]  := '00';
          StringGrid1.Cells[1,0]  := '01';
          StringGrid1.Cells[2,0]  := '02';
          StringGrid1.Cells[3,0]  := '03';
          StringGrid1.Cells[4,0]  := '04';
          StringGrid1.Cells[5,0]  := '05';
          StringGrid1.Cells[6,0]  := '06';
          StringGrid1.Cells[7,0]  := '07';
          StringGrid1.Cells[8,0]  := '08';
          StringGrid1.Cells[9,0]  := '09';
          StringGrid1.Cells[10,0] := '0A';
          StringGrid1.Cells[11,0] := '0B';
          StringGrid1.Cells[12,0] := '0C';
          StringGrid1.Cells[13,0] := '0D';
          StringGrid1.Cells[14,0] := '0E';
          StringGrid1.Cells[15,0] := '0F';

          while (True) do begin
                if FS.Read(Tmp,1) < 1  then Break;//ファイル終了マーカーが検出されたらBreakする
                StringGrid1.Cells[X,Y] := IntToHex(Tmp,2);

                if (Tmp < $20) or (Tmp > $7F) then
                   StringGrid2.cells[X,Y] := '.'
                else
                   StringGrid2.Cells[X,Y] := Chr(Tmp);

                Inc(X);
                if X > 15 then begin
                   Inc(Y);
                   X := 0;
                end;
          end;

          StringGrid1.RowCount := Y+1;
          StringGrid2.RowCount := Y+1;
      finally
         FS.Free;
         StringGrid1.Visible := True;
         StringGrid2.Visible := True;
      end;

   end;
end;


にしの  2004-03-03 00:38:42  No: 7466

>jokさん
私の場合、速度云々はコーディング後の調整で、まずはわかりやすく書くべきと思ってます。
# さすがにFileStreamで1バイトずつは通常やりません^^;
はなさんが最初に、
> 1バイトずつ切り出してHex変換しています
と記述されていたので、それにあわせて作りました。

さらにjokさんへです。
IntToHexはasmで書かれていますので、Pascalでこれより早くできるかどうか・・・。
でも面白そうですね。
こういう小さいルーチンをどこまで早く出来るかとか。

話が脱線しました。
すみません。


jok  2004-03-03 00:57:20  No: 7467

>にしのさん

>私の場合、速度云々はコーディング後の調整で、まずはわかりやすく書くべきと思ってます。

はい、わたしもそうしてます。しかし今回は速度が遅いのでどうにかしたい、とい
のが主眼でしたので、思いっきりポインタを使ってしまいました。(笑)

実際のところ TFileStream と TMemoryStream では Read Write を使う限り
あまり速度の差は無いようです。しかし、TMemoryStream ではポインタが使える
ので、ポインタを使う限りでは値のコピーをしなくて済みますのでそれだけ
速いです。

もっとも変換速度に影響するのは、文字列の継ぎ足しをするかどうかです。

  st := st + nanika;

というのはものすごく効率悪いです。1メガバイトのファイルで最後に10バイト
を継ぎ足すとき、文字列全体がコピーされることもあり得ます。これが、何千回
以上も起こることを想像すると、ちょっとたじろぎます。文字列が長ければ
長いほど、継ぎ足しによる効率の低下はひどくなります。

今回の場合のように桁数が一定の場合、IntToHex() と ByteToHexStr() の
どちらが速いか試してました。

procedure TForm1.Button1Click(Sender: TObject);
var
  i:integer;
  st,ed:DWORD;
  b:Byte;
  s:string;
begin
  st := GetTickCount;
  for i := 1 to 10000 do
    for b := 0 to $FF do
      s := ByteToHexStr(b);
  ed := GetTickCount;
  Label1.Caption := IntToStr(ed-st);
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  i:integer;
  st,ed:DWORD;
  b:Byte;
  s:string;
begin
  st := GetTickCount;
  for i := 1 to 10000 do
    for b := 0 to $FF do
      s := IntToHex(b,2)+' ';
  ed := GetTickCount;
  Label2.Caption := IntToStr(ed-st);
end;

わたしの環境では ByteToHexStr() のほうが6倍くらい速いです。


にしの  2004-03-03 01:32:07  No: 7468

なるほど。
文字列の拡張時は確かに遅いです。

少しだけ、ByteToHexStrを改良しました。
ほんの数ミリ秒早くなりました^^;
本当は、ASMに変換して、余分なコードをさらに削りたかったんですが、DelphiのASMコード出力がわからず断念。
Local変数へのコピー分、余分なコードがあるはずです。

function MyIntToHex(const value:byte): String;
var
  p: pbyte;
begin
  SetLength(Result, 3);
  p := PBYTE(Result);
  p^ := value shr 4;
  if p^ > 10 then p^ := p^ + ($41 - 10) else p^ := p^ + ($39 - 10);
  Inc(p);
  p^ := value and 15;
  if p^ > 10 then p^ := p^ + ($41 - 10) else p^ := p^ + ($39 - 10);
  Inc(p);
  p^ := $20;
end;


にしの  2004-03-03 01:33:52  No: 7469

間違い。
($39 - 10);
は、
$39;
でした。


はな  2004-03-03 02:07:44  No: 7470

>jokさん、にしのさん

ご教授ありがとうございます。アドバイス通り
最初にStringをサイズ分で確保して、改行の置換も
止めたところ早くなりました。

貴重なご意見ありがとうございます。

>LupinⅢさん

ご回答ありがとうございます。
StringGridはまだ試せていませんが、
やってみようと思います。

皆さん、ありがとうございました。
勉強になりました。


※返信する前に利用規約をご確認ください。

※Google reCAPTCHA認証からCloudflare Turnstile認証へ変更しました。






  このエントリーをはてなブックマークに追加