19 декабря 2019

Кто такой TBufferedFileStream?

    Недавно в модуле System.Classes под описанием класса TFileStream я случайно заметил наследуемый от него класс TBufferedFileStream. Судя по документации, TBufferedFileStream добавляет поддержку буферизации в TFileStream, тем самым оптимизируя небольшие последовательные операции чтения/записи файла. Давайте разберемся, что это за зверь, когда и откуда он появился, и, что он дает?
    Итак, проведем небольшое расследование. Судя по истории изменения документации (Revision history of "System.Classes.TBufferedFileStream") описание этого класса было добавлено в документацию 15 февраля 2016‎ года. Т.е. перед выпуском RAD Studio 10.1 Berlin. Согласно "What Was New in Berlin": класс TFDFileStream был перемещен из FireDAC.Stan.Util в System.Classes и переименован в TBufferedFileStream. Ну вот, даже те, кто не пользуются FireDAC, получили пользу от того, что компания Embarcadero купила себе AnyDAC.
    Происхождение этого класса мы установили. Теперь давайте посмотрим на сколько он хорош. Напишем две процедуры, которые построчно читают текстовый файл. Первая использует для этого TFileStream:
procedure TestFileStream(const sFileName: String);
var
  fs: TFileStream;
  sr: TStreamReader;
  sw: TStopwatch;
  st: String;
begin
  sw := TStopwatch.StartNew;
  fs := TFileStream.Create(sFileName, fmOpenRead);
  try
    sr := TStreamReader.Create(fs);
    try
      while not sr.EndOfStream do
        st := sr.ReadLine;
    finally
      sr.Free
    end;
  finally
    fs.Free;
  end;
  sw.Stop;
  WriteLn(st.Chars[0], ' ', sw.Elapsed.ToString);
end;
Вторая использует для чтения файла TBufferedFileStream:
procedure TestBufferedFileStream(const sFileName: String; 
                                 const iBufferSize: Integer);
var
  fs: TBufferedFileStream;
  sr: TStreamReader;
  sw: TStopwatch;
  st: String;
begin
  sw := TStopwatch.StartNew;
  fs := TBufferedFileStream.Create(sFileName, fmOpenRead, iBufferSize);
  try
    sr := TStreamReader.Create(fs);
    try
      while not sr.EndOfStream do
        st := sr.ReadLine;
    finally
      sr.Free
    end;
  finally
    fs.Free;
  end;
  sw.Stop;
  WriteLn(st.Chars[0], ' ', sw.Elapsed.ToString);
end;
    Когда то давно я реализовывал свой класс для чтения и парсинга CSV-файлов, и он так же как TBufferedFileStream был наследован от TFileStream и так же для ускорения чтения использовал буфер. Похоже это очередное подтверждение теоремы о бесконечных обезьянах... :-) Так вот, когда я тестировал свой класс, максимальной производительности на различных файлах он у меня достигал при буфере размером в 1 МБ. Поэтому во вторую процедуру я добавил параметр "размер буфера" (iBufferSize) и выполню ее с двумя различными размерами буфера - 32 КБ (размер буфера по умолчанию из конструктора класса TBufferedFileStream) и 1 МБ (размер буфера понравившийся мне ранее).

Тест 1. Файл 1 000 000 строк (173 МБ)
Тест Время выполнения Прирост производительности, %
TFileStream 00:01.9145231
TBufferedFileStream и буфер 32КБ 00:01.7441268 8.90
TBufferedFileStream и буфер 1МБ 00:01.7336918 9.45

Тест 2. Файл 10 000 000 строк (1,77 ГБ)
Тест Время выполнения Прирост производительности, %
TFileStream 00:19.7432841
TBufferedFileStream и буфер 32КБ 00:17.9686645 8.99
TBufferedFileStream и буфер 1МБ 00:17.8678814 9.50

Тест 3. Файл 100 000 000 строк (18,6 ГБ)
Тест Время выполнения Прирост производительности, %
TFileStream 03:23.0220558
TBufferedFileStream и буфер 32КБ 03:05.4529768 8.65
TBufferedFileStream и буфер 1МБ 03:03.5610330 9.59

Как видите использование TBufferedFileStream для последовательного чтения файла дает стабильный прирост производительности. А это значит, он рекомендован к использованию!

Примечания:
  1. Если из файлового потока читать по одному байту, а не как я строками через TStreamReader, то разница в скорости может быть более значительной. Но только кому это надо?
  2. TBufferedFileStream не дает такого прироста производительности, когда размер буфера меньше, чем читаемая из файлового потока порция данных.
  3. TBufferedFileStream не дает такого прироста производительности для случайных операций чтения/записи.

1 комментарий:

  1. Анонимный14 мая, 2024 11:43

    procedure TestBufferedFileStream(const sFileName: String;
    const iBufferSize: Integer);
    var
    fs: TBufferedFileStream;
    sr: TStreamReader;
    sw: TStopwatch;
    st: TStringList;
    begin
    sw := TStopwatch.StartNew;
    fs := TBufferedFileStream.Create(sFileName, fmOpenRead, iBufferSize);
    try
    sr := TStreamReader.Create(fs);
    try
    st := TStringList.Create;
    try
    while not sr.EndOfStream do
    st.Add(sr.ReadLine);
    finally
    sr.Free;
    end;
    finally
    fs.Free;
    end;
    finally
    sw.Stop;
    WriteLn('Read ', st.Count, ' lines in ', sw.Elapsed.ToString);
    st.Free;
    end;
    end;

    procedure TForm1.Button1Click(Sender: TObject);
    begin
    TestBufferedFileStream('test.txt', 4096);
    end;

    ОтветитьУдалить