可変長データの循環バッファによる書き込みの最適な方法は?

解決


take  2011-03-07 18:02:50  No: 40180

TListに参照させているクラスを
ファイルに書き込む処理があります。

件数が2万件以上あるため負荷が高く困っています。

書き込むタイミングはTListに追加されてから
一定時間は待つ「遅延書き込み」を行っていますが

それでも負荷が高いのは変わりません。

一定件数以上のデータが追加された分は
古い順に消していきます。

これをTStringStreamなどのストリームクラスを使用して
循環バッファが使えれば負荷が減るかと考えています。

固定長なら最終書き込み位置を管理すれば
循環バッファが作成できそうなのですが
可変長の場合は無理なのでしょうか?

【イメージ】
00003 ← 最終書き込み位置
xxxx 99999999 xxxxxxxxxxxxxxx 
xxxx 99999999 xxxxxxxxxxxxxxxxxxxxxx
xxxx 99999999 xxxxxxxxxxxxxxx         ←最後に書き込まれた部分
xxxx 99999999 xxxxxxxxxxxxx           ←最も古いデータ


KHE00221  2011-03-08 05:11:18  No: 40181

最も古いデータが一番下にあるということは・・・
単純にファイルに追加していってない?

>一定件数以上のデータが追加された分は
>古い順に消していきます。

負荷とはこの事?

何をさしているのかがわからないw

>固定長なら最終書き込み位置を管理すれば

可変長でも Indexファイル作れば できるとおもうよ?

古い順に消すのではなく
ファイル切り替えちゃったほうが早いと思うけど!


take  2011-03-08 17:20:59  No: 40182

レスありがとうございます。

2万件のデータをファイルに出力するのに時間がかかり
そこが負荷となって現れています。

>古い順に消すのではなく
>ファイル切り替えちゃったほうが早いと思うけど!

ほかの箇所はログデータなのでそのようにしています。

そこで同じようにTMemoryStreamやTFileStreamを使って
古い順に消すほうのログも扱えないかなと思いました。

>可変長でも Indexファイル作れば できるとおもうよ?

ファイルに対してSeekする量が不明になるので
どのようなIndexファイルを作ればいいのか想像つきません。

この辺を参考にして作り始めてみました。
http://www.ichibachi.com/delphi/bufstream.html


KHE00221  2011-03-09 04:50:33  No: 40183

2万件のデータをファイルに出力するのに時間がかかり
そこが負荷となって現れています。

2万件一気に吐き出してる???

>書き込むタイミングはTListに追加されてから
>一定時間は待つ「遅延書き込み」を行っていますが

じゃなくて少量ずつ吐き出したほうがいいと思う!

>一定件数以上のデータが追加された分は
>古い順に消していきます。

となると一定件数っていくつ?

>ファイルに対してSeekする量が不明になるので
>どのようなIndexファイルを作ればいいのか想像つきません。

テキストファイルだとして  
各行の位置(バイト数)を保存してくだけ
それで頭5行消すとしたら  5行目のIndexを見て
Read側で6行目の頭までSeekして残りをWrite
Indexずれるので修正忘れずに


KHE00221  2011-03-09 06:37:23  No: 40184

あとは別スレッドにするってのもありかなと


take  2011-03-10 01:03:22  No: 40185

> 2万件一気に吐き出してる???
> じゃなくて少量ずつ吐き出したほうがいいと思う!

たしかにおっしゃる通りなのですが
はき出している途中に強制シャットダウンする可能性もあるので
その都度はき出す仕様としています。

スレッドでも同様ですね。

>>一定件数以上のデータが追加された分は
>>古い順に消していきます。

となると一定件数っていくつ?

書き方が悪かったですね。
一定件数は2万件です。

ただし設定で最大保持件数を変更できる仕様のため
その辺もややっこしくなっています。

> テキストファイルだとして  
> 各行の位置(バイト数)を保存してくだけ
> それで頭5行消すとしたら  5行目のIndexを見て
> Read側で6行目の頭までSeekして残りをWrite
> Indexずれるので修正忘れずに

なるほど、各行のIndexを持つわけですね。

謎なのが下記のような時です。3件保持と仮定します。

--- 1件追加 ---
xxxxxxx
--- 2件追加 ---
xxxxxxx
yyyyy
--- 3件追加 ---
xxxxxxx
yyyyy
zzzzzzzzzz
--- 4件追加 ---
aaaaaxx
yyyyy
zzzzzzzzzz

4件目追加の時1件目が不要なので
そこにseekして書き込めばいいかなと考えて
でもそこに書き込むと可変長のため
前に書き込まれているデータ長と合わないため
可変長データで循環バッファファイルは使えないなと思いました。

もうちょっと考えたいと思います。


KHE00221  2011-03-10 04:47:06  No: 40186

>たしかにおっしゃる通りなのですが
>はき出している途中に強制シャットダウンする可能性もあるので
>その都度はき出す仕様としています。

強制シャットダウンってのはいきなり電源OFFとかでなければ

少量づつ吐き出して、シャットダウン感知したら
シャットダウン待たせて
残ってるの全部吐き出すようにすれば
いいだけじゃないのか?

シャットダウン感知時の大量出力の負荷は関係ないだろ?


KHE00221  2011-03-10 04:51:04  No: 40187

メモリ上にある件数が2万件で

一度に吐き出す件数が2万件で

>一定件数以上のデータが追加された分は
>古い順に消していきます。
>書き方が悪かったですね。
>一定件数は2万件です。

ということは

2万件丸ごと入れかえじゃないのか?


D  2011-03-10 08:31:22  No: 40188

いっそ一件一ファイルにしてしまうとか。
2万個のファイルが出来てしまうけれどもランダムアクセスはやりやすくなるだろうし。
FindFirstやFindNextでワイルドカードが使えるので検索や絞込みもファイル名を工夫すればそれほど苦労せずにできそうでもあるし。
ファイルの更新日時でソートすれば書き込み順で読み込めるし。


HOta  2011-03-10 16:26:25  No: 40189

こんなことをするのがデーターベースの機能でしょう。
データーベースに任せたらどうでしょう。


take  2011-03-10 17:56:41  No: 40190

レスありがとうございます。
>>一定件数は2万件です。
>ということは
>2万件丸ごと入れかえじゃないのか?

20,000件までは蓄積して
20,001件目の書き込みで1件目は削除
という処理です。

>いっそ一件一ファイルにしてしまうとか。
それおもしろい考え方ですね。
専用のフォルダを作ってしまえば結構楽かもしれません。

>こんなことをするのがデーターベースの機能でしょう。
>データーベースに任せたらどうでしょう。

やはりそうですかね・・・
「数件まで蓄積して古い順に削除」
と言う処理を頻繁に使うため
早くて安全な処理にできないかと思っていたのですが

何パターンか作成してみたいと思います。
ありがとうございました。


take  2011-03-10 18:35:31  No: 40191

解決とさせて頂きます


monaa  2011-03-10 23:10:00  No: 40192

出遅れましたが、
私も以前データロガーで同じような事を妄想したことが有ります。
せっかくの機会なので書いてみました。
ただTListは循環式のデータを扱うのに適していないので独自クラスで書きました。
原理は
ディスク領域に対して  
  データ1.........
  データ2.......................
  データ3...
  データ4..........
  データ5...............
とデータ追加時に追加分のデータをファイルに書き込み
データサイズが規定を超えたら
  データ6......................
  データ3...
  データ4..........
  データ5...............
と古いデータのあったファイル領域に新しいデータを書きこみます。
メモリー内ではでは古いデータは消去、
新しい領域に新しいデータを格納します。
つまり、全データ読み込み式、逐次書き込みタイプのデータベースです。

サンプルの意味を込めてなるべく簡潔に書こうと努力しましたが、
読み返してみるとちょっと読みづらいですね。
要約するとTVariableDataが前後のデータと手を繋ぐ形のデータ型で、
1-2-3-4-5
  2-3-4-5-6
とデータの前後の追加削除に最適な形式を取っています。
TVariableDataListはそれを包括したクラスで、ファイル出力を備えています。
数キロのファイルでしか実験していませんが、理論上はどこまで大きなファイルサイズでも同じ速度で書き込み出来るはずです。

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, ComCtrls, StdCtrls;

type
  TVariableData = class(TObject)
  private
    procedure SetNext(const Value: TVariableData);
    procedure SetPre(const Value: TVariableData);
  public
    ID:Integer;  //追加順ユニークID
    Str:string;  //可変長データ
    fPre  :TVariableData;
    fNext:TVariableData;
    function Size:Integer;
    procedure WriteFile(aStream:TFileStream);
    procedure SaveToStream(aStream:TMemoryStream);
    function LoadFromStream(aStream:TMemoryStream):Boolean;
    property Next:TVariableData read fNext write SetNext;
    property Pre :TVariableData read fPre  write SetPre;
  end;

  //全データをメモリに保持する方式
  //ハードディスクへの書き込みはリアルタイム
  TVariableDataList = class(TObject)
  private
    LastDataPos,FirstDataPos:Integer;
  public
    MaxSize:Integer;    //ディスクに格納できる最大サイズ
    fFileName:string;
    FirstData,LastData: TVariableData;
    constructor Create;
    function AddData(str:string):Integer;
    procedure Clear;
    procedure RemoveMinimum;
    procedure LoadFromFile(aFileName:string);
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    ListView1: TListView;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
    fDataFile:string;
    VariableDatList: TVariableDataList;
  public
    { Public 宣言 }
    procedure ListUp;
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

{ TVariableData }

function TVariableData.LoadFromStream(aStream: TMemoryStream):Boolean;
var
  strLen:Integer;
begin
  if aStream.Position + SizeOf(Integer)*2  > aStream.Size then
  begin
    Result := False;
    Exit;
  end;
  aStream.Read(ID,Sizeof(Integer));
  aStream.Read(strLen,Sizeof(Integer));

  if aStream.Position + strLen*2  > aStream.Size then
  begin
    Result := False;
    Exit;
  end;

  SetLength(Str,strLen);
  aStream.Read(Str[1],strLen*2);
  Result := True;
end;

procedure TVariableData.SaveToStream(aStream: TMemoryStream);
var
  strLen:Integer;
begin
  aStream.Write(ID,Sizeof(Integer));
  strLen := Length(Str);
  aStream.Write(strLen,Sizeof(Integer));
  aStream.Write(Str[1],strLen*2);
end;

procedure TVariableData.SetNext(const Value: TVariableData);
begin
  if Value<>nil then
    Value.fPre := Self;
  fNext := Value;
end;

procedure TVariableData.SetPre(const Value: TVariableData);
begin
  if Value<>nil then
    Value.fNext := Self;
  fPre := Value;
end;

function TVariableData.Size: Integer;
begin
  Result := Sizeof(Integer)*2 +
            Length(Str)*2;
end;

procedure TVariableData.WriteFile(aStream: TFileStream);
var
  strLen:Integer;
begin
  aStream.Write(ID,Sizeof(Integer));
  strLen := Length(Str);
  aStream.Write(strLen,Sizeof(Integer));
  aStream.Write(Str[1],strLen*2);
end;

{ TVariableDataList }

function TVariableDataList.AddData(str: string): Integer;
var
  aData:TVariableData;
  aSize,aSize2:Integer;
  aFileStream:TFileStream;
begin
  aData := TVariableData.Create;
  aData.Str := str;
  if FirstData=nil then
  begin
    FirstData := aData;
    aData.ID := 0;
  end else
    aData.ID := LastData.ID +1;
  Result := aData.ID;
  aData.Pre := LastData;
  LastData := aData;
  aFileStream := TFileStream.Create(fFileName,fmOpenReadWrite);
  aSize := aData.Size;
  if (LastDataPos < MaxSize) and
     (LastDataPos >= FirstDataPos) then
  begin
    //最大サイズ以下の場合
    aFileStream.Position := LastDataPos + SizeOf(Integer)*2;
    aData.WriteFile(aFileStream);
    LastDataPos := LastDataPos + aSize;
  end else begin
    //最大サイズ超過
    aSize2 := 0;
    repeat
      aSize2 := aSize2 + FirstData.Size;
      RemoveMinimum;
    until aSize2 >= aSize;
    FirstDataPos:= FirstDataPos + aSize2;
    if FirstDataPos >= MaxSize then
      FirstDataPos := 0;
    if LastDataPos >= MaxSize then
      LastDataPos := 0;

    aFileStream.Position := LastDataPos + SizeOf(Integer)*2;
    aData.WriteFile(aFileStream);
    LastDataPos := LastDataPos + aSize;
  end;
  aFileStream.Position:=0;
  aFileStream.Write(FirstDataPos,SizeOf(Integer));
  aFileStream.Write(LastDataPos,SizeOf(Integer));
  aFileStream.Free;
end;

procedure TVariableDataList.Clear;
var
  aData1,aData2:TVariableData;
begin
  aData1 := FirstData;
  while aData1<>nil do
  begin
    aData2 := aData1.Next;
    aData1.Free;
    aData1 := aData2;
  end;
  FirstData := nil;
  LastData  := nil;
  FirstDataPos:= 0;
  LastDataPos := 0;
end;

constructor TVariableDataList.Create;
begin
  inherited;
  fFileName := 'data.dat';
  FirstData := nil;
  LastData  := nil;
  FirstDataPos:= 0;
  LastDataPos := 0;
  MaxSize  := 256*2*10;    //ディスクに格納できる最大サイズ
end;

procedure TVariableDataList.LoadFromFile(aFileName: string);
var
  aStream:TMemoryStream;
  aData,aDataPre:TVariableData;
  aCanLoad:Boolean;
begin
  Clear;
  fFileName := aFileName;
  if FileExists(aFileName)=False then
  begin
    aStream := TMemoryStream.Create;
    aStream.SaveToFile(aFileName);
    aStream.Free;
    Exit;
  end;
  aStream := TMemoryStream.Create;
  aStream.LoadFromFile(aFileName);
  aStream.Read(FirstDataPos,SizeOf(Integer));
  aStream.Read(LastDataPos,SizeOf(Integer));
  aStream.Position:=FirstDataPos + SizeOf(Integer)*2;
  aDataPre := nil;
  repeat
    aData := TVariableData.Create;
    aCanLoad := aData.LoadFromStream(aStream);
    if aCanLoad then
    begin
      aData.Pre := aDataPre;
      aDataPre := aData;
      if FirstData = nil then
        FirstData := aData;
      LastData := aData;
    end;
  until (aCanLoad=False);

  aStream.Position := SizeOf(Integer)*2;
  if LastDataPos < FirstDataPos then
  repeat
    aData := TVariableData.Create;
    aCanLoad := aData.LoadFromStream(aStream);
    if aCanLoad then
    begin
      aData.Pre := aDataPre;
      aDataPre := aData;
      LastData := aData;
    end;
  until aStream.Position + SizeOf(Integer)*2 >= LastDataPos;
  aStream.Free;
end;

procedure TVariableDataList.RemoveMinimum;
begin
  FirstData := FirstData.next;
  FirstData.pre.Free;
  FirstData.pre:=nil;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to 100 - 1 do
  begin
    VariableDatList.AddData(DateTimeToStr(Now)+ ' - ' +IntToStr(i));
  end;
  ListUp;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  VariableDatList := TVariableDataList.Create;
  fDataFile := ExtractFilePath(Application.ExeName)+'test.dat';
  VariableDatList.LoadFromFile(fDataFile);
  ListUp;
end;

procedure TForm1.ListUp;
var
  item:TListItem;
  aData: TVariableData;
begin
  ListView1.Clear;
  aData := VariableDatList.FirstData;
  while aData <> nil do
  begin
    item := ListView1.Items.Add;
    item.Caption := IntToStr(aData.ID);
    item.SubItems.Add(aData.Str);
    aData := aData.Next;
  end;
end;

end.


monaa  2011-03-10 23:15:30  No: 40193

一応ですが、D2009なのでStringのサイズを2倍しています。
Sizeof(Char)で書き忘れていますので、D7の場合はご注意を。


take  2011-03-11 00:21:00  No: 40194

monaa様へ
貴重なソースの提示をありがとうございます。
非常に参考になります。

原理はまさにその通りです!!。
多少の断片化は仕方ないと思っています。


KHE00221  2011-03-11 04:51:27  No: 40195

>こんなことをするのがデーターベースの機能でしょう。
>データーベースに任せたらどうでしょう。

データベースはデータを削除しないのでファイルはどんどんでかくなる。

最近のは実際にデータを削除してくるのか?


KHE00221  2011-03-11 04:55:59  No: 40196

ところで
全レコード同じサイズなのか?


HOta  2011-03-11 05:03:35  No: 40197

>データベースはデータを削除しないのでファイルはどんどんでかくなる。

>最近のは実際にデータを削除してくるのか?

データーベースによります。
削除した場所にデーターを書き込むものもあります。


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

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






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