戻る ホーム 上へ 進む

ストリームをバッファ付きにしたい

Tip&解説

Delphi に付属している THandleStream や TFileStream はバッファリングされていません。ですから、Read/ReadBuffer、Write/WriteBuffer メソッドで小さなデータを読み書きすると、著しく遅くなってしまいます。

オペレーティングシステムに対しての I/O 要求は、読み書きするデータが少ない場合でも、ある程度の時間が必要です。ですから 1バイト、2バイトといった小さなデータの I/O をオペレーティングシステムに要求することは極端に非効率です。

このような場合、読み込みの際にはデータをある程度先読みしたり、書き込みの際には書き込むべきデータをバッファにためて、適当な時期に一挙に書き出すと効率的です。これを Buffered I/O といいます。C/C++ では標準で「標準入出力」というバッファつきの I/O ライブラリが付属しているので、C/C++ のプログラマはそれがバッファつき I/O であることを知っているかいないかにかかわらず、自然にバッファつき I./Oを使っています。

逆に Delphi プログラマは普段バッファ無しのストリームを使っていますが、これは時にトラブルの元になります。単に遅いだけならまだよいのですが、例えば、ネットワーク上の共有ファイルをバッファリングせずに数バイト単位で大量にアクセスすると、ネットワークの障害が起きることさえあります。

バッファリングをサポートする各種ストリームを新たに作ることもできますが、既存のストリームにバッファリング機能を加えることはそれほど難しいことではありません。

以下に紹介するストリーム TNkBufferedStream はストリームをバッファつきにするストリームです。これを使えば、例えば

var
  FS: TFileStream:
  BS: TNkBufferedStream;
   :
   :
  FS := TFileStream('XXXX.dat', fmOpenReadWrite);
   :
   :
  BS := TNKBufferedStream(FS, 4096); // FS をバッファ付きにする。バッファ長は4096
   :
   :
  BS.Write(......);
  BS.ReadBuffer(......);
   :
   :
  BS.Free;
   :
   :
  FS.Free;

というように、ファイルストリーム(FS)に対してバッファつきストリーム(BS)を作成し、FSのメソッドを使う代わりに BS のメソッドを使ってファイルを読み書きすれば、より効率的なファイルアクセスが行えます。

TNkBufferedStream はどのようなストリームでもバッファ付きに変身させることができます。尚、TNkBufferedStream は Delphi 5/6 でも使えますが、Delphi 6 では 64bit のストリームもサポートしています。

---------- TNkBufferedStream のソースコード ----------

{$IFDEF CONDITIONALEXPRESSIONS}
{$DEFINE DELPHI6ORLATER}
{$ENDIF}

// 2002/7/11 追加
{$RANGECHECKS OFF}
// 2003/4/17 Seek の Offset の符号が soFromEnd時 逆に処理されていたので
// 修正しました。

unit NkStream;

interface

uses
  SysUtils, Classes;

type
  ENkSTreamError = class(Exception);

  TNkStreamMode = (nkSmNoBuffering, nkSmRead, nkSmWrite);

  TNkBufferedStream = class(TStream)
  private
    FMode: TNkStreamMode;
    FBuffer: array of Byte;  // バッファ
    FBufferSize: Integer;    // バッファサイズ
    FDataSize: Integer;      // バッファ内のデータサイズ
    FDataIndex: Integer;     // 次に読むデータのインデックス
    FStream: TStream;        // バッファリング対象のストリーム

    procedure FillBuffer;    // バッファに先読みする

    // バッファ状態を考慮した正しい位置を返す。
    function GetCorrectPosition: Int64;
    procedure EmptyBuffer;   // バッファをクリアし非バッファリング状態にする
  protected
    // ストリームのリサイズ 32bit版
    procedure SetSize(NewSize: Longint); override;

    {$IFDEF DELPHI6ORLATER}
    // ストリームのリサイズ 64bit版
    procedure SetSize(const NewSize: Int64); override;
    {$ENDIF}
    
  public
    constructor Create(Stream: TStream; BufferSize: Integer);
    destructor Destroy; override;

    function Read(var Buffer; Count: Longint): Longint; override;
    function Write(const Buffer; Count: Longint): Longint; override;
    function FlushBuffer: Integer;

    function Seek(Offset: Longint; Origin: Word): Longint;
      overload; override;

    {$IFDEF DELPHI6ORLATER}
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
      overload; override;
    {$ENDIF}
    
  end;

implementation

procedure RaiseError(s: string);
begin raise ENkStreamError.Create(s); end;

{ TNkBufferedStream }

// コンストラクタ
constructor TNkBufferedStream.Create(Stream: TStream; BufferSize: Integer);
begin
  inherited Create;

  if (BufferSize < 1) then
    RaiseError('TNkBufferedStream.Create: バッファサイズが不正です');

  FStream := Stream; // ストリームを格納

  // バッファを初期化
  FBufferSize := BufferSize;
  SetLength(FBuffer, FBufferSize);
  FDataSize := 0;
  FDataIndex := 0;

  FMode := nkSmNoBuffering; //バッファリングなし状態
end;

// デストラクタ
destructor TNkBufferedStream.Destroy;
begin
  FlushBuffer; // 書き込みキャッシュにデータがあれば書き込む
  inherited;
end;

// バッファを空にし nkSmNoBuffering モードにする
procedure TNkBufferedStream.EmptyBuffer;
begin
  case FMode of
    nkSmRead:  // 先読みを取り消す
      begin
        FMode := nkSmNoBuffering;
        FStream.Position := FStream.Position - (FDataSize - FDataIndex);
        FDataSize := 0;
        FDataIndex := 0;
      end;
    nkSmWrite:
      begin
        FlushBuffer; // データバッファを吐く
        FDataSize := 0;
        FDataIndex := 0;
      end;
  end;
  FMode := nkSmNoBuffering; // バッファリングが無い状態へ
end;

// バッファに先読みを行う
procedure TNkBufferedStream.FillBuffer;
begin
  Assert(FMode = nkSmRead);
  FDataSize := FStream.Read(FBuffer[0], FBufferSize);
  FDataIndex := 0;
end;

// バッファにたまったデータを吐き出す。書き込んだバイト数を返す。
function TNkBufferedStream.FlushBuffer: Integer;
begin
  Result := 0;
  if FMode = nkSmWrite then // 書き込みモード時のみフラッシュを行う
  begin
    Result := FStream.Write(FBuffer[0], FDataIndex);
    FDataIndex := 0;
  end;
end;

// 先読み、ライトバッファの格納データ長を考慮した正しい Position を得る
function TNkBufferedStream.GetCorrectPosition: Int64;
begin
  Result := 0; // ダミー
  case FMode of
    nkSmNoBuffering: Result := FStream.Position;
    nkSmRead:        Result := FStream.Position - (FDataSize - FDataIndex);
    nkSmWrite:       Result := FStream.Position + FDataIndex;
  end;
end;

// 読み込み
function TNkBufferedStream.Read(var Buffer; Count: Integer): Longint;
var
  Dest: PChar;               // 書き込み先
  DataSizeInBuffer: Integer; // 先読みバッファ中のデータ量
  ReadLength: Integer;       // 読み込む長さ
begin
  if Count < 0 then
    RaiseError('TNkBufferedStream.Read: Count が負です');

  // 読み込みモードで無いならば、
  // バッファを空にしてモードを切り替えて先読み
  if FMode <> nkSmRead then
  begin
    EmptyBuffer;
    FMode := nkSmRead;
    FillBuffer;
  end;

  Result := 0;     // 累積読み込み量 クリア
  Dest := @Buffer; // Dest を書き込み先バッファの先頭へ

  while True do    // 読み込みループ
  begin
    if FDataSize = 0 then Exit; // 読むデータがなくなれば終了

    // 読み込みサイズを決める
    DataSizeInBuffer := FDataSize - FDataIndex;
    if DataSizeInBuffer < Count then ReadLength := DataSizeInBuffer
                                else ReadLength := Count;

    // 読み込む
    System.Move(FBuffer[FDataIndex], Dest^, ReadLength);
    Inc(Result, ReadLength);
    Dec(Count, ReadLength);
    Inc(Dest, ReadLength);
    Inc(FDataIndex, ReadLength);

    if Count = 0 then Exit; // 全て読んだら終了

    FillBuffer; // バッファが空なので先読みをする
  end;
end;

// 32bit版 Seek
function TNkBufferedStream.Seek(Offset: Integer; Origin: Word): Longint;
var
  RequestedPosition: Int64;
begin
  RequestedPosition := 0; // ダミー

  // 要求された位置を計算する
  case Origin of
    soFromBeginning: RequestedPosition := Offset;
    soFromCurrent:   RequestedPosition := GetCorrectPosition + Offset;
    soFromEnd:
      begin
        EmptyBuffer;
        RequestedPosition := FStream.Size + Offset;
      end;
  end;

  if GetCorrectPosition <> RequestedPosition then // 位置に変化がある
  begin
    EmptyBuffer; // バッファを空にする
    FStream.Position := RequestedPosition; // 格納しているストリームに
                                           // 位置を設定
  end;

  Result := GetCorrectPosition;
end;

{$IFDEF DELPHI6ORLATER}
// 64bit版 Seek
function TNkBufferedStream.Seek(const Offset: Int64;
  Origin: TSeekOrigin): Int64;
var
  RequestedPosition: Int64;
begin
  RequestedPosition := 0; // ダミー

  // 要求された位置を計算する
  case Origin of
    soBeginning: RequestedPosition := Offset;
    soCurrent:   RequestedPosition := GetCorrectPosition + Offset;
    soEnd:
      begin
        EmptyBuffer;
        RequestedPosition := FStream.Size - Offset;
      end;
  end;
  if GetCorrectPosition <> RequestedPosition then // 位置に変化がある
  begin
    EmptyBuffer; // バッファを空にする
    FStream.Position := RequestedPosition; // 格納しているストリームに
                                           // 位置を設定
  end;
  Result := GetCorrectPosition;
end;
{$ENDIF}

// サイズへ変更
procedure TNkBufferedStream.SetSize(NewSize: Longint);
begin
  EmptyBuffer;             // バッファを空にする
  FStream.Size := NewSize; // バッファリング対象のStream をリサイズ
end;

{$IFDEF DELPHI6ORLATER}
procedure TNkBufferedStream.SetSize(const NewSize: Int64);
begin
  EmptyBuffer;             // バッファを空にする
  FStream.Size := NewSize; // バッファリング対象のStream をリサイズ
end;
{$ENDIF}

// 書き込み
// 制限事項: Write が返す書き込みバイト数はバッファに書き込まれたものを
//            含むので、実際にストリームに書き込まれたものでは無い。
//            フラッシュに失敗した場合不一致が生じる。
function TNkBufferedStream.Write(const Buffer; Count: Integer): Longint;
var
  RestOfBuffer: Integer; // ライトバッファの残量
  Source: PChar;         // 読み込み元
  WriteLength: Integer;  // 書き込み量
begin
  if Count < 0 then
    RaiseError('TNkBufferedStream.Write: Count が負です');


  // 書き込みモードで無いならバッファをクリアして書き込みモードに
  if FMode <> nkSmWrite then
  begin
    EmptyBuffer;
    FMode := nkSmWrite;
  end;

  Result := 0;
  Source := @Buffer;

  while True do
  begin
    // 書き込み量を決める
    RestOfBuffer := FBufferSize - FDataIndex;
    if RestOfBuffer < Count then WriteLength := RestOfBuffer
                            else WriteLength := Count;

    // 書き込む
    System.Move(Source^, FBuffer[FDataIndex], WriteLength);
    Inc(FDataIndex, WriteLength);
    Inc(Result, WriteLength);
    Dec(Count, WriteLength);
    Inc(Source, WriteLength);

    if Count = 0 then Exit; // 書き終わったら終了

    // 全部かけない場合も終了(Disk Full?)
    if FlushBuffer < FBufferSize then Exit;
  end;
end;

end.

---------- TNkBufferedStream のソースコード  おわり ----------

戻る ホーム 上へ 進む

inserted by FC2 system