Как загрузить очень большой текстовый файл в TStringList?

Delphi 11/12, работа с большими текстовыми файлами.

Простой код:

var slist: TStringList;
slist := TStringList.Create;
slist.LoadFromFile(ParamStr(1));

вызывает ошибку в версии 11

Exception EIntOverflow in module PrimitiveMotif.exe at 0000000000010DEA, integer overflow

и ошибку в версии 12

Exception EAbstractError in module PrimitiveMotif.exe at 0000000000044191, abstract error

Файл - текстовый, огромный. Но "всего лишь" 1.5 Гб, FAR показывает всего 18 с небольшим миллиона строк, что вполне вписывается в ограничение TStringList. Памяти на компьютере - залейся (192 Гб). Куда копать?

P.S. Строки - ANSI


Ответы (1 шт):

Автор решения: MBo

Если посмотреть, как работает TStrings.LoadFromFile, то оно загружает (LoadFromStream) содержимое файла в массив байтов, затем этот массив преобразует в строку GetString, затем разбирает юникодную строку (я наверняка ещё не все шаги учёл). Таким образом, в памяти одновременно тройной-четверной объём файла находится, выделяется три или четыре непрерывных гигабайтных куска памяти - не слишком разумно и ловко получается, и менеджер памяти не справляется, похоже.

Однако при необходимости держать такой TStringList в памяти можно читать из файла построчно в список:

  • старыми методами вроде Readln - ну теперь это не рекомендуется

  • самостоятельно разбирать файл по строкам (я когда-то делал чтение из Memory-mapped файлов гигантских логов, было втрое быстрее Readln во времена ANSI строк, да и в 32-битные времена целиком просто нельзя было прочитать те файлы)

  • использовать TStreamReader.ReadLine - это, наверное, самый оптимальный с точки зрения трудозатрат метод, по скорости не проверял.

  • опять же самостоятельно разбирать файл по строкам с использованием хорошо проработанного BufferedFileStream от David Heffernan

P.S. И всё-таки все строки одновременно нужны? И база данных тут не подойдёт?

Небольшой тест. Создаётся файл размером 4 гигабайта, содержащий 16 миллионов Ansi-строк длиной 256 (с учётом CRLF).

//создаём файл
procedure TForm2.Button1Click(Sender: TObject);
var
  s: AnsiString;
  i: Integer;
  f: TextFile;
begin
   AssignFile(F,'d:\testlong.txt');
   ReWrite(F);
   for i := 1 to 16*1048576 do begin
     s := StringOfChar(AnsiChar(48 + i mod 10), 254);
     Writeln(f, s);
   end;
   CloseFile(F);
end;

procedure TForm2.Button2Click(Sender: TObject);
var
  s: AnsiString;
  i: Integer;
  f: TextFile;
  sl: TStringList;
  t: DWord;
  Buff: array[0..4095] of Char;
begin
  t := GetTickCount;
  sl := TStringList.Create;
  AssignFile(f, 'd:\testlong.txt');
  Reset(f);

  /////
  TTextRec(f).BufSize := SizeOf(Buff);
  TTextRec(f).BufPtr := @Buff;
  /////

  i := 0;
  while not Eof(f) do
  begin
    Readln(f, s);
    if i mod 4 = 0 then
      sl.Add(s);
    Inc(i);
  end;
  CloseFile(f);
  Memo1.Lines.Add((GetTickCount - t).ToString);
  Memo1.Lines.Add(sl.Count.ToString);
  for i := 0 to 7 do // control
    Memo1.Lines.Add(sl[i]);
  sl.Free;
end;


procedure TForm2.Button3Click(Sender: TObject);
const
  BufSize = 1048576;
var
  st: TReadOnlyCachedFileStream;
  Buf: TBytes;
  Readed, i, start, cnt: Integer;
  sl: TStringList;
  astr, aleft: AnsiString;
  t: DWord;
begin
  t:= GetTickCount;
  sl := TStringList.Create;
  cnt := 0;
  astr := '';
  aleft := '';
  SetLength(Buf, BufSize);
  st := TReadOnlyCachedFileStream.Create('d:\testlong.txt');
  Readed := st.Read(Buf[0], BufSize);
  while Readed > 0 do begin
    start := IfThen(Buf[0] = 10, 1, 0);
    for i := 0 to readed - 1 do begin
      if Buf[i] = 13 then begin
        if cnt mod 4 = 0 then begin
          SetString(astr, PAnsiChar(@Buf[start]), i - start);
          sl.Add(aleft + astr);
          end;
        start := i + 2;
        aleft := '';
        inc(cnt);
      end;
    end;
    if start < readed then
      SetString(aleft, PAnsiChar(@Buf[start]), readed - start)
    else
      aleft := '';
    Readed := st.Read(Buf[0], BufSize);
  end;
  Memo1.Lines.Add((GetTickCount - t).ToString);
  Memo1.Lines.Add(sl.Count.ToString);
  for i := 0 to 7 do   //control
    Memo1.Lines.Add(sl[i]);
  for i := 0 to 7 do // control
    Memo1.Lines.Add(sl[sl.Count - i - 1]);
  st.Free;
  sl.Free;
end;

procedure TForm2.Button4Click(Sender: TObject);
var
  Reader: TStreamReader;
  s: AnsiString;
  i: integer;
  sl: TStringList;
  t: DWord;
 begin
  t := GetTickCount;
  sl := TStringList.Create;
  i := 0;
  Reader := TStreamReader.Create('d:\testlong.txt', TEncoding.ANSI);
  while not Reader.EndOfStream do begin
    s := Reader.ReadLine;
    if i mod 4 = 0 then
      sl.Add(s);
    inc(i);
  end;
  Memo1.Lines.Add((GetTickCount - t).ToString);
  Memo1.Lines.Add(sl.Count.ToString);
  for i := 0 to 7 do   //control
    Memo1.Lines.Add(sl[i]);
  for i := 0 to 7 do // control
    Memo1.Lines.Add(sl[sl.Count - i - 1]);
  Reader.Free();
  sl.Free;

end;

Используется SSD-диск с SATA (скорость его ~600 MB/s).

Readln работает минуту без улучшения буферизации, и всего 8 секунд с увеличенным буфером. Каждую четвертую строку извлекает. Два гигабайта памяти в этот момент программа занимает, не вылетает.

С использованием модуля буферизованного чтения и самостоятельного разбора по CRLF (подправил попадание LF в начало буфера) работает тоже 6-8 секунд.

С использованием TStreamReader (на SSD не проверял пока)

На HDD и более старом компе времена порядка 28, 21 и 24 сек. Это близко к пределу пропускной способности диска, так что и StreamReader вполне нормально себя ведёт.

P.S. Новомодный TFile.ReadAllLines тоже обломался прочитать данный файл целиком (чтение сделано примерно так же, как у списка).

P.P.S. Ещё можно побаловаться с GPTextFile от автора OmniThreads, но не думаю, что особо что-то удастся выиграть уже. Исходная проблема действительно существует (пример на EnSO, в общем-то совпадает с тем, что я написал), но в вашем случае пока обходится тем, что список целиком загружать необязательно.

→ Ссылка