環境:Win2k、Delphi6(Personal)
はじめまして。
文字列処理について教えていただきたいことがあります。
電文で送信したデータをダンプ出力する為に、1バイトずつ
切り出してHex変換しています。サイズが小さいデータなら
早いのですが、サイズが大きいデータになるとこの変換処理に
かなりの時間がかかります。
変換処理はrepeat 〜 untilの中で文字列1byte切り出しの
IntToHexで変換し、String型変数に退避しています。
(出力イメージはB'Z等のダンプ出力ツール)
75Kのデータで行ってみたところ、最後まで待つ事が
出来ないくらいの時間です(5分位は待ちました)。
高速に処理する方法等あるのでしょうか?
(.NETにあるStringBuilderみたいなものとか)
ご教授お願い致します。
環境にも夜かと思いますが、72.8KBのファイルを1バイトずつ読み込んで16進数にし、出力するものを試してみると、こちらでは300ミリ秒程度で終了しています。
何か別のところに間違いはありませんか?
# カウントに16bit変数を使用しているとか
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;
にしのさん、jokさん、
早速のご回答ありがとうございます。
質問の記述で間違いがありました。
75Kではなく750Kです。申し訳ありません。
マシンスペックはPentiumM 1.6GHz、メモリ1Gです。
>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;
上と同じコードで、2.67MBのデータを処理させたところ、10025 msでした。
Pen4の1.7GHz、メモリ384MBです。
コーディングはどうなってます?
> にしのさん
ByteToHexStr() は IntToHex() より速いと思ってわざわざ作りました。
> FileStreamで1バイトずつ取り込んでいるので、こっちの方が遅いかも。
確実に遅いでしょう。メモリ+ポインタより速いアクセス方法はありません。
> TMemoryStreamで読み込む場合、数GBになると問題が出てきそうです。
お題は1Mバイト以下です。1000倍以上のサイズを心配する必要はないのでは?
はなです。
ダンプ変換のソースを載せてみます。
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;
これは、バイナリエディタの画面みたいに16進表示とその横に文字列を表示して
いるのですね。
1行ごとに StringReplace() を2度も実行しているのは非常に無駄です。
1バイトごとに値を変換しているんですから、そのとき値を調べて #13 の
ときと #10 ときを自力で変換したほうがいいです。
また、
dump := dump + msg;
こんなふうに文字列を継ぎ足していくのは、メモリの再確保・コピーが頻発して
遅い原因になります。ファイルの大きさが分かれば何行になるかあらかじめ計算
できますので、SetLength() で文字列の長さを確保しておき、内容を1バイト
ずつ入れ替えていくかまとめてコピーするようにすると、効率が上がります。
横から失礼します。
こんなのはどうでしょう?
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;
>jokさん
私の場合、速度云々はコーディング後の調整で、まずはわかりやすく書くべきと思ってます。
# さすがにFileStreamで1バイトずつは通常やりません^^;
はなさんが最初に、
> 1バイトずつ切り出してHex変換しています
と記述されていたので、それにあわせて作りました。
さらにjokさんへです。
IntToHexはasmで書かれていますので、Pascalでこれより早くできるかどうか・・・。
でも面白そうですね。
こういう小さいルーチンをどこまで早く出来るかとか。
話が脱線しました。
すみません。
>にしのさん
>私の場合、速度云々はコーディング後の調整で、まずはわかりやすく書くべきと思ってます。
はい、わたしもそうしてます。しかし今回は速度が遅いのでどうにかしたい、とい
のが主眼でしたので、思いっきりポインタを使ってしまいました。(笑)
実際のところ 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倍くらい速いです。
なるほど。
文字列の拡張時は確かに遅いです。
少しだけ、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;
間違い。
($39 - 10);
は、
$39;
でした。
>jokさん、にしのさん
ご教授ありがとうございます。アドバイス通り
最初にStringをサイズ分で確保して、改行の置換も
止めたところ早くなりました。
貴重なご意見ありがとうございます。
>LupinⅢさん
ご回答ありがとうございます。
StringGridはまだ試せていませんが、
やってみようと思います。
皆さん、ありがとうございました。
勉強になりました。
ツイート | ![]() |