Delphi中的文本文件写入性能

6
我的程序正在处理来自Telnet、HTTP等的字符串,并且我需要将这些字符串写入文本文件进行日志记录。有时程序可能会崩溃,我需要确保剩余的字符串不会丢失,因此我为每个传入的字符串打开/关闭文件,但这样做会导致一些性能问题。例如,下面的代码需要8秒才能完成。我的代码如下,有没有改进性能的方法?(对于下面的测试,只需创建一个带Button1的表单,具有OnClick事件和lbl1标签即可。)
Procedure AddToFile(Source: string; FileName :String);
var
  FText : Text;
  TmpBuf: array[word] of byte;
Begin
  {$I-}
  AssignFile(FText, FileName);
  Append(FText);
  SetTextBuf(FText, TmpBuf);
  Writeln(FText, Source);
  CloseFile(FText);
  {$I+}
end;

procedure initF(FileName : string);
Var  FText : text;
begin
  {$I-}
  if FileExists(FileName) then  DeleteFile(FileName);
  AssignFile(FText, FileName);
  ReWrite(FText);
  CloseFile(FText);
  {$I+}
end;

procedure TForm1.Button1Click(Sender: TObject);
var tTime : TDateTime;
    iBcl : Integer;
    FileName : string;
begin
  FileName := 'c:\Test.txt';
  lbl1.Caption := 'Go->' + FileName; lbl1.Refresh;
  initF(FileName);
  tTime := Now;
  For iBcl := 0 to 2000 do
    AddToFile(IntToStr(ibcl) + '   ' +  'lkjlkjlkjlkjlkjlkjlkj' , FileName);
  lbl1.Caption  :=  FormatDateTime('sss:zzz',Now-tTime);
end;

2
打开/关闭文件是一项耗时的操作。为什么不在内存中打开它一次,并将文件指针保留在某个位置?另外,文本文件使用可能会出现延迟,请尝试使用TFileStream并通过字节块写入数据。为确保数据被推送到文件中,请使用FileStream.Flush。 - Nickolay Olshevsky
你应该在初始化文件后保持其打开状态,然后将FText变量传递或保持为全局变量... - user497849
在我看来,你正在处理症状而非原因... 找出崩溃发生的位置,将try ... except块放在那里并在那里处理文件关闭 - 然后如果此时程序已经死亡,请重新引发异常。 - Despatcher
4个回答

15

使用自动缓冲并能够自动刷新其缓冲区到TFileStreamTStreamWriter。如果需要,它还允许您选择追加到现有文件,设置字符编码以支持Unicode,并在其不同的重载Create构造函数中设置不同的缓冲区大小(默认值为1024字节或1K)。

请注意,仅刷新TStreamWriterTStreamBuffer的内容写入TFileStream;它不会刷新操作系统文件系统缓冲区,因此直到释放TFileStream时,文件才真正写入磁盘。

不要每次都创建StreamWriter,只需创建并打开一次,在结束时关闭:

function InitLog(const FileName: string): TStreamWriter;
begin
  Result := TStreamWriter.Create(FileName, True);
  Result.AutoFlush := True;         // Flush automatically after write
  Result.NewLine := sLineBreak;     // Use system line breaks
end;

procedure CloseLog(const StreamWriter: TStreamWriter);
begin
  StreamWriter.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var 
  tTime : TDateTime;
  iBcl : Integer;
  LogSW: TStreamWriter;
  FileName: TFileName;
begin
  FileName := 'c:\Test.txt';
  LogSW := InitLog(FileName);
  try
    lbl1.Caption := 'Go->' + FileName; 
    lbl1.Refresh;
    tTime := Now;

    For iBcl := 0 to 2000 do
      LogSW.WriteLine(IntToStr(ibcl) + '   ' +  'lkjlkjlkjlkjlkjlkjlkj');

    lbl1.Caption  :=  FormatDateTime('sss:zzz',Now - tTime);
  finally
    CloseLog(LogSW);
  end;
end;

TStreamWriter不会刷新操作系统文件缓冲区;而且它也不允许设置自己的缓冲区大小,如果需要性能,则变得无用。 - kludg
1
@Serg:它确实允许设置自己的缓冲区大小。请查看文档;可以通过使用重载构造函数的其中一种版本来完成(正如我在上面的答案中所说)。请参阅链接的文档。它还会刷新操作系统缓冲区-请参阅“AutoFlush”上的文档。也许您习惯于较旧版本的StreamWriter? - Ken White
抱歉,我错过了重载构造函数。忘记了我关于缓冲区大小的说法。 :) - kludg
请问给我点踩的人可以解释一下我的回答有什么问题吗? - Ken White
请注意与AutoFlush及其功能相关的更新答案。 - Ken White

3

不必重新打开文件以将关键数据保存到磁盘,您可以使用FlushFileBuffers函数或通过调用CreateFile函数打开一个无缓冲I/O的文件,并使用FILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGH标志 (请参阅第一个链接中的Remarks部分)。


FILE_FLAG_NO_BUFFERING 的对齐要求意味着它并不是非常实用。 - David Heffernan
@DavidHeffernan - 如果你自己进行缓存,那么这是实际可行的;数据要么写入磁盘,要么在你的缓冲区中。 - kludg
那部分并不是很实用。必须编写缓冲代码。还不如使用 FILE_FLAG_WRITE_THROUGHFlushFileBuffers - David Heffernan

2

看起来你的问题是每次写入后需要清除缓存,这样如果应用程序崩溃,就不会丢失数据。

虽然我相信这里的其他答案都很好,但你不需要对代码进行如此广泛的更改。你只需要在每次写入后调用 Flush(FText) 即可。

const
  // 10 million tests
  NumberOfTests = 1000000;

  // Open and close with each write:        19.250 seconds

  // Open once, and flush after each write:  5.686 seconds

  // Open once, don't flush                  0.439 seconds

var
  FText : Text;
  TmpBuf: array[word] of byte;

procedure initF(FileName : string);
begin
  {$I-}
  if FileExists(FileName) then  DeleteFile(FileName);
  AssignFile(FText, FileName);
  ReWrite(FText);
  SetTextBuf(FText, TmpBuf);
  {$I+}
end;

procedure CloseTheFile;
begin
  CloseFile(FText);
end;

Procedure AddToFile(Source: string);
Begin
  {$I-}
  Writeln(FText, Source);

  // flush the cache after each write so that data will be written
  // even if program crashes.
  flush ( fText );              // <<<====   Flush the Cache after each write

  {$I+}
end;

procedure TForm1.Button1Click(Sender: TObject);
var tTime : TDateTime;
    iBcl : Integer;
    FileName : string;
begin
  FileName := 'c:\Test.txt';
  lbl1.Caption := 'Go->' + FileName; lbl1.Refresh;
  initF(FileName);

  // put file close in a try/finally block to ensure file is closed
  // even if an exception is raised.
  try

    tTime := Now;
    For iBcl := 0 to NumberOfTests-1 do
      AddToFile(IntToStr(ibcl) + '   ' +  'lkjlkjlkjlkjlkjlkjlkj');
    lbl1.Caption  :=  FormatDateTime('sss:zzz',Now-tTime);

  finally
    CloseTheFile;
  end;
end;

顺便提一下,如果文件已经存在,Rewrite会清空文件,所以不需要先删除文件。 - David Dubois

1
由于某些原因,我发现从一个文本文件读取并写入另一个文本输出文件时,TextFile的WriteLn仍然是最快的方法。
  AssignFile(t,'c:\a\in.csv');
  Reset(t);
  AssignFile(outt,'c:\a\out.csv');
  ReWrite(outt);
  while not eof(t) do
  begin
    Readln(t,x);
    WriteLn(outt, x);   //27 sec, using LogSW.WriteLine(outx) takes 54 sec

//使用上述代码处理半个G文件只需27秒,而使用Martijn提供的TStreamWriter示例需要54秒:o


StreamReader sr = new StreamReader(location + "\" + singleFilename); StreamWriter sw = new StreamWriter(outputFolder + "\" + singleFilename);(注:这是一段C#代码,目的是创建一个StreamReader对象来读取指定路径下的文件,并创建一个StreamWriter对象将内容写入到指定输出文件夹中) - Max DiBiagio

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接