Как загрузить очень большой текстовый файл в 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 шт):
Если посмотреть, как работает 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, в общем-то совпадает с тем, что я написал), но в вашем случае пока обходится тем, что список целиком загружать необязательно.