Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2016年12月25日] [更新 2016年12月25日] Delphi サンプルプログラム集
202_プロセスで使用可能なメモリ量やデータサイズ等
動作確認等 Windows 7 U64(SP1) + Delphi XE(UP1) Pro
202_LimitedMemoryDataSize.zip [2,436 KB] 2016年12月25日版 (EXE 同梱)







プロセスで使用可能なメモリ量やデータサイズ等


1 つのプログラム,プロセスで使用可能なメモリには上限があります.また,テキスト等の表示用のコントロールに表示可能なデータサイズにも上限があります.
本ページはそれらを確認するための記事です.





01_プロセス ( プログラム ) で使用可能なメモリ量


プログラムが扱える以上のメモリを必要とする場合,あるいは,テキスト等を表示するコントロールで扱えるデータ量には制限があります.この制限を超えると,
      「メモリが足りません」
というエラーが発生します.
32 ビットのコンピュータの場合,Windows も Linux もプロセスが使用するのは,最大 4 GB です.この内,Windows では 2 GB がカーネルモードとして使われ,残りの 2 GB がユーザモードのアドレス空間として割り当てられます.1 つのプロセスではこれ以上のメモリは使用できません.
この 2 GB も,ヒープ領域やオブジェクトの制御等に使用されますから,実際に利用可能なのは更に限られます.Windows API を使用する場合は,実行時に読み込まれる DLL もメモリを消費します.
TStringList を使用して,ファイルからテキストを読み込む祭も,実際には,テキストの容量分以上のメモリを消費します.

64 ビットのアプリケーションでは,使用可能なユーザモードのアドレス空間は 8 TB です.
もちろん,これらのアドレス空間が実際に利用可能なわけではありません.システムによって,使用可能な物理的なメモリ量には制限があります.


図1
メモリが足りません
  • TMemo に 400 MB のテキストデータを読み込んだ場合

この「メモリが足りません」の場合,SysErrorMessage(GetLastError) での表示は,下図のように,
    「このコマンドを実行するのに十分な記憶域がありません。」
となります.


図2
エラーメッセージが異なる
  • エラーの内容が同じでも,エラーの検出方法によってメッセージは異なる




02_読み込むデータ量の制限


表示やオブジェクトに読み込むデータの制限は,メモリ量だけではありません.次のコードは,このことを確認するためのテストです.
このテストでは,TStringList に文字列を追加していきますが,なるべくメモリを使用しないように空文字をセットしています.追加する数が 100000000 の時は正常に終了しますが, 2 倍の 200000000 になると「リストの容量が超えました」となり,セットできなくなります.
コントロールには,このように,設定可能な項目数に制限がある場合もあります.



参考リンクの記事にある MaxListSize の値は,以下のように定義されています.MaxInt は System のグローバル定数として定義されています.


const
  MaxListSize = Maxint div 16; //Classes.pas
  MaxInt = High(Integer);      //System


図3
セット可能な範囲内の場合
図4
制限を超えた場合

procedure TForm1.Button1Click(Sender: TObject);
const
  ACount = 100000000;
var
  ACnt : Integer;
  SL   : TStringList;
  i    : Integer;
begin
  ACnt := 100000000;
  if CheckBox1.Checked then ACnt := ACnt * 2;

  SL := TStringList.Create;
  try
    for i := 0 to ACnt - 1 do begin
      SL.Add('');
    end;
  finally
    SL.Free;
  end;
  Application.MessageBox(PChar(UIntToStr(i)), '結果');
end;


読み込む項目数以外にも,コントロールには,読み込み,あるいは表示可能なデータ量に制限がある場合もあります.
TMemo でこの制限を超えると,下図のように「文字列の大きさがメモの制限を超えました」というダイアログが表示されます.このエラーは,読み込もうとするテキストのサイズにもよります.そのサイズによっては「メモリ不足」のエラーとなる場合もあります.


図5
これも制限を超えた場合
  • 例えば,下のコードで,400 MB のテキストデータを読み込んだ場合

procedure TForm1.Button1Click(Sender: TObject);
var
  FileName : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if not OpenTextFileDialog1.Execute then exit;


  FileName  := OpenTextFileDialog1.FileName;

  Memo1.Lines.Clear;
  Memo1.Lines.LoadFromFile(FileName);
end;


ネット上で,大量のデータの表示を高速化したい,というような質問を見かけることがあります.データベースのデータを TBDGrid に何万レコードとか,TStringGrid に何万行,何 10 万行とかというのもあります.しかし,表示するということは,ユーザが目で見るということです.

「目で見る」ということは,確認作業に近いことになるでしょう.テキストファイルのテキストもそうですが,それだけ大量のデータを表示する必要性が,本当にあるのかということも検討してもいいのではないでしょうか.

インターネットの検索エンジンで,検索結果が 100 万件あったとします.実現の可能性はともかくとして,この全てをスクロール表示で見えるようにした方が合理的で便利なのでしょうか.
とは言っても,人は理屈で行動するわけではありません.時には,クライアントの無理な,理屈に合わない,非合理的な要求にも応えなければならない.というのも事実なのかも知れません.




03_ファイルサイズを GetFileSizeEx 関数で取得


使用可能なメモリに制限があるのであれば,あらかじめ読み込むテキストファイルのサイズを知っておくのも,アプリケーションの開発で必要になるかも知れません.
サンプルプログラム集には,ファイルのプロパティとしてのファイルサイズの取得のサンプルはありますが,以下は,ファイルのサイズだけを取得する GetFileSizeEx 関数の使用例です.


図6
実行結果例

リスト1
ファイルサイズを GetFileSizeEx 関数で取得
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

// GetFileSizeExはDelphi XEには定義がないので定義
function GetFileSizeEx(hFile: THandle; var lpFileSize: LARGE_INTEGER): BOOL; stdcall;
  external kernel32 name 'GetFileSizeEx';

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'ファイルサイズの取得';
  InitDir := '';
end;

//=============================================================================
//  CreateFile関数でファイルのハンドルを取得して
//  GetFileSizeEx関数を使用してファイルのサイズをを調べる
//
//  timeGetTimeの使用には,usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName  : String;
  StartTime : DWORD;
  EndTime   : DWORD;
  hFile     : THandle;
  LargInt   : LARGE_INTEGER;
  FileSize  : Int64;
  StrRes    : String;
begin
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  hFile := Windows.CreateFile(PChar(Filename),
                              GENERIC_READ,
                              0,
                              nil,
                              OPEN_EXISTING,
                              FILE_ATTRIBUTE_NORMAL,
                              0);
  if hFile = INVALID_HANDLE_VALUE then exit;

  try
    if GetFileSizeEx(hFile, LargInt) = False then exit;
  finally
    Windows.CloseHandle(hFile);
  end;

  FileSize := LargInt.LowPart or (Int64(LargInt.HighPart) shl 32);
  EndTime  := timeGetTime;
  StrRes   := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
              FormatFloat('#,###' , FileSize);
  Application.MessageBox(PChar(StrRes), '情報');
end;

end.


ファイルハンドルの値は,次のように FileOpen 関数でも取得できます.
FileOpen 関数は CreateFile のラッパー関数です.内部で CreateFile を使用しています.


procedure TForm1.Button1Click(Sender: TObject);
var
  FileName  : String;
  StartTime : DWORD;
  EndTime   : DWORD;
  hFile     : THandle;
  LargInt   : LARGE_INTEGER;
  FileSize  : Int64;
  StrRes    : String;
begin
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;

  hFile := FileOpen(FileName, fmOpenRead);
  try
    if GetFileSizeEx(hFile, LargInt) = False then exit;
  finally
    FileClose(hFile);
  end;

  FileSize := LargInt.LowPart or (Int64(LargInt.HighPart) shl 32);
  EndTime  := timeGetTime;
  StrRes   := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
              FormatFloat('#,###' , FileSize);
  Application.MessageBox(PChar(StrRes), '情報');
end;




04_ファイルのサイズを TFileStreamで取得


ファイルのサイズは,TFileStream を使用したファイルストリームのサイズでも取得できます.その確認用のサンプルプログラムです.


図7
実行結果例

リスト2
ファイルのサイズを TFileStream 等で取得
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenTextFileDialog1: TOpenTextFileDialog;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'ファイルサイズの取得';
  InitDir := '';
end;

//=============================================================================
//  TFileStreamを使用してファイルのサイズをを調べる
//  ストリームのサイズがファイルのサイズ
//
//  timeGetTimeの使用には,usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName  : String;
  StartTime : DWORD;
  EndTime   : DWORD;
  FStream   : TFileStream;
  FileSize  : Int64;
  StrRes    : String;
begin
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;
  FStream   := TFileStream.Create(FileName, fmOpenRead);
  try
    FStream.Seek(0, soEnd);
    FileSize := FStream.Position;

  finally
    FStream.Free;
  end;

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
             FormatFloat('#,###' , FileSize);
  Application.MessageBox(PChar(StrRes), '情報');
end;

//=============================================================================
//  TStreamReaderを使用してファイルのサイズをを調べる
//  ストリームのサイズがファイルのサイズ
//  Delphi 2009以降専用
//
//  timeGetTimeの使用には,usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  StartTime : DWORD;
  EndTime   : DWORD;
  FileName  : String;
  SReader   : TStreamReader;
  FileSize  : Int64;
  StrRes    : String;
begin
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  FileName  := OpenTextFileDialog1.FileName;
  StartTime := TimeGetTime;
  //ファイルの読み込み準備。文字コードはOSのデフォルト
  SReader := TStreamReader.Create(FileName, TEncoding.Default);
  try
    SReader.OwnStream;
    FileSize := SReader.BaseStream.Size;
  finally
    SReader.Close;
    SReader.Free;
  end;

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
             FormatFloat('#,###' , FileSize);
  Application.MessageBox(PChar(StrRes), '情報');
end;

end.




05_テキストファイルの行数を FileSystemObject で取得


コントロールに表示,あるいは設定可能な項目数の制限があれば,あらかじめテキストファイルの行数を知ることも,場合によっては必要となるかも知れません.

テキストファイルの行数を知るには,改行文字を調べることになります.しかし,ファイルの文字を 1 バイトあるいは 1 文字単位で検査すると非常に時間がかかります.
ファイルのテキストを行単位で読み出すと,行単位でのポインタの移動となり,高速に処理できるようになります.実際に全ての行を読み出すわけですから,サイズの大きなファイルは当然,時間がかかることになります.
ここでは,FileSystemObjectを使用して,テキストファイルの行数を調べます.



図8
FileSystemObject で行数を調べる
  • 500 MB のテキストファイルの結果

リスト3
テキストファイルの行数を FileSystemObject で取得
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses Scripting_TLB, MMSystem, ComObj;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'テキストファイルの行数取得';
  InitDir := '';
end;

//=============================================================================
//  FileSystemObjectオブジェクトでテキストファイルの行数を調べる
//  バイナリファイルのサイズは取得できない
//
//  IFileSystem,TextStreamの使用には,usesにScripting_TLBとComObjが必要
//  timeGetTimeの使用には,usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName   : String;
  StartTime  : DWORD;
  EndTime    : DWORD;
  ilineCount : Int64;
  Fso        : IFileSystem;
  FsoTS      : TextStream;
  StrRes     : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;
  Fso       := CoFileSystemObject.Create;
  try
    //追加モードでファイルを開いて,ファイルの最後に移動く
    FsoTS := Fso.OpenTextFile(FileName, ForAppending, False, TristateUseDefault);
    ilineCount := FsoTS.Line;
  finally
    FSo := nil;
  end;

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
             IntToStr(ilineCount);
  Application.MessageBox(PChar(StrRes), '情報');
end;

end.




06_テキストファイルの行数を TextFile オブジェクトで取得


TextFile オブジェクトを使用して,テキストファイルの行数を調べるサンプルです.
TextFile オブジェクトでは,SetTextBuf 関数で,キャッシャとしてのバッファリングサイズを設定できます.この値を適切にすると,より高速な処理が可能です.下図の例では,FileSystemObject を使用した場合の 3 分の 1 程度の時間で取得できています.
これなら,テキストファイル内の文字列を検索する機能を実装したとしても,ある程度は実用に耐えるかも知れません


図9
TextFile オブジェクトで行数を調べる
  • 500 MB のテキストファイルの結果

リスト4
テキストファイルの行数を TextFile オブジェクトで取得
unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    Button1: TButton;
    OpenTextFileDialog1: TOpenTextFileDialog;
    procedure Button1Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private 宣言 }
    InitDir : string;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses MMSystem;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := 'テキストファイルの行数取得';
  InitDir := '';
end;

//=============================================================================
//  TextFile型でテキストファイルの行数を調べる
//  バイナリファイルのサイズは取得できない
//
//  timeGetTimeの使用には,usesにMMSystemが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  FileName   : String;
  StartTime  : DWORD;
  EndTime    : DWORD;
  hFile      : TextFile;
  Buff       : array [0..$40000-1] of Char;
  sLine      : String;
  iLineCount : Int64;
  StrRes     : String;
begin
  OpenTextFileDialog1.Filter := 'テキストファイル|*.txt';
  if InitDir = '' then begin
    OpenTextFileDialog1.InitialDir := '..\..\TextFiles';
  end else begin
    OpenTextFileDialog1.InitialDir := InitDir;
  end;
  if not OpenTextFileDialog1.Execute then exit;
  InitDir := ExtractFileDir(OpenTextFileDialog1.FileName);


  StartTime := TimeGetTime;
  FileName  := OpenTextFileDialog1.FileName;
  AssignFile(hFile, FileName);
  System.SetTextBuf(hFile, Buff);
  Reset(hFile);

  iLineCount := 0;
  while not EOF(hFile) do begin
    ReadLn(hFile, sLine);
    iLineCount := iLineCount + 1;
    Application.ProcessMessages;
  end;
  CloseFile(hFile);

  EndTime := timeGetTime;
  StrRes  := IntToStr(EndTime - StartTime) + ' ms' + sLineBreak +
             IntToStr(ilineCount);
  Application.MessageBox(PChar(StrRes), '情報');
end;

end.