TStringList.LoadFromFile — Исключения с большими текстовыми файлами

Я использую Delphi RAD Studio XE2.

У меня есть несколько очень больших файлов, каждый из которых содержит большое количество строк. Сами строки небольшие - всего 3 дубля, разделенных табуляцией. Я хочу загрузить файл в TStringList с помощью TStringList.LoadFromFile, но это вызывает исключение с большими файлами.

Для файлов из 2 миллионов строк (примерно 1 ГБ) я получаю исключение EIntOverflow. Для больших файлов (например, 20 миллионов строк и примерно 10 ГБ) я получаю исключение ERangeCheck.

У меня есть 32 ГБ ОЗУ для игры, и я просто пытаюсь загрузить этот файл и использовать его быстро. Что здесь происходит и какие еще у меня есть варианты? Могу ли я использовать файловый поток с большим буфером для загрузки этого файла в TStringList? Если да, то не могли бы вы привести пример.


person Trojanian    schedule 19.11.2014    source источник
comment
Я просто должен задаться вопросом, почему вы загружаете 20 миллионов строк текста? Возможно, вам повезет больше, если вы воспользуетесь TFileStream.   -  person Jerry Dodge    schedule 19.11.2014
comment
У вас есть пример, показывающий, как использовать TFileStream для чтения строк текстового файла в TStringList?   -  person Trojanian    schedule 19.11.2014
comment
Я бы предпочел хранить строки файла в таблице в базе данных. Манипуляции тогда будут намного быстрее, чем использование потомков T*List. Итак, вопрос в том, что вы собираетесь делать с данными?   -  person iPath ツ    schedule 19.11.2014
comment
Проще говоря, реальное решение состоит в том, чтобы прекратить попытки загрузить весь файл в память.   -  person David Heffernan    schedule 19.11.2014


Ответы (1)


Когда Delphi перешла на Unicode в Delphi 2009, метод TStrings.LoadFromStream() (который TStrings.LoadFromFile() вызывает внутри) стал очень неэффективным для больших потоков/файлов.

Внутренне LoadFromStream() считывает весь файл в память как TBytes, затем преобразует его в UnicodeString с помощью TEncoding.GetString() (который декодирует байты в TCharArray, копирует его в конечный UnicodeString, а затем освобождает массив) , затем анализирует UnicodeString (пока TBytes все еще находится в памяти), добавляя подстроки в список по мере необходимости.

Таким образом, непосредственно перед выходом LoadFromStream() в памяти есть четыре копии файловых данных — три копии занимают в худшем случае filesize * 3 байт памяти (где каждая копия использует свой собственный непрерывный блок памяти + некоторый MemoryMgr накладные расходы) и одну копию для проанализированных подстрок! Конечно, первые три копии освобождаются при фактическом выходе LoadFromStream(). Но это объясняет, почему вы получаете ошибки памяти до достижения этой точки - LoadFromStream() пытается использовать 3-4 ГБ памяти для загрузки файла размером 1 ГБ, и диспетчер памяти RTL не может с этим справиться.

Если вы хотите загрузить содержимое большого файла в TStringList, вам лучше использовать TStreamReader вместо LoadFromFile(). TStreamReader использует буферизованный файловый ввод-вывод для чтения файла небольшими порциями. Просто вызовите его метод ReadLine() в цикле, Add() переводя каждую строку в TStringList. Например:

//MyStringList.LoadFromFile(filename);
Reader := TStreamReader.Create(filename, true);
try
  MyStringList.BeginUpdate;
  try
    MyStringList.Clear;
    while not Reader.EndOfStream do
      MyStringList.Add(Reader.ReadLine);
  finally
    MyStringList.EndUpdate;
  end;
finally
  Reader.Free;
end;

Возможно, когда-нибудь LoadFromStream() можно будет переписать, чтобы использовать TStreamReader внутри, как это.

person Remy Lebeau    schedule 19.11.2014
comment
И если вы знаете, сколько строк, используйте sl.Capacity := KnownValue; для предотвращения множественных вызовов ReallocMem(). - person Gerry Coll; 19.11.2014
comment
TStringList не вызывает ReallocMem() для каждого Add(), он экспоненциально увеличивает свою память. - person Remy Lebeau; 19.11.2014
comment
Память перераспределяется только тогда, когда текущий Count равен Capacity при добавлении новой строки. Capacity растет (в элементах количество байтов будет Capacity*SizeOf(TStringItem) плюс небольшие накладные расходы MemoryMgr) следующим образом: 0,4,8,12,28,44,60,76,95,118,147,183,228,285,356,445,556,... - person Remy Lebeau; 19.11.2014
comment
Даже если вы точно не знаете, сколько элементов списка есть/будет, огромный прирост производительности можно получить, предварительно установив значение емкости на репрезентативно большое число (наилучшее предположение, если сможете). а затем установить его на фактический счет, когда элементы закончат загрузку, чтобы восстановить любые «отходы». В этом случае можно сделать хорошее предположение о требуемой емкости, учитывая, что формат каждой строки в файле известен (три двойных разделителя табуляции): емкость := размер файла / средняя длина строки - person Deltics; 19.11.2014
comment
@RemyLebeau Спасибо за это. Я тестирую его сейчас, и он решает мою проблему (по крайней мере, для файлов размером 5 ГБ). Как я могу настроить его, чтобы улучшить производительность? Использует ли ваше решение размер буфера по умолчанию? Как изменить размер буфера? Кроме того, в некоторых случаях (не во всех) я знаю количество строк и формат каждой строки. - person Trojanian; 19.11.2014
comment
@RemyLebeau - никогда не говорил, что он растет каждый раз. Он увеличивается на 25%, когда достигает своей емкости. Старые версии использовали ReallocMem, новые используют SetLength, но используют дельту текущей емкости / 4 - person Gerry Coll; 19.11.2014
comment
@Trojanian: TStreamReader по умолчанию использует буфер размером 4 КБ, но вы можете указать другой размер буфера в конструкторе. И существует множество сторонних реализаций буферизованного ввода-вывода TFileStream. - person Remy Lebeau; 19.11.2014
comment
@RemyLebeau: тогда мне нужен перегруженный конструктор System.Classes.TStreamReader.Create(const Filename: string; Encoding: TEncoding; DetectBOM: Boolean = False; BufferSize: Integer = 1024). Что такое детектбом? - person Trojanian; 19.11.2014
comment
@GerryColl: Как предварительно установить емкость в приведенном примере кода ответа? - person Trojanian; 19.11.2014
comment
@Trojanian: да, это конструктор для использования. DetectBOM сообщает читателю, может ли он просмотреть начало файла, чтобы увидеть, есть ли BOM указание кодировки данных. В противном случае необходимо указать кодировку в параметре Encoding. Поскольку вы загружаете текстовый файл, а TStreamReaderTStringList) работает со строками Unicode, программа чтения должна знать кодировку файла, чтобы при чтении он мог декодировать текст в Unicode. - person Remy Lebeau; 19.11.2014
comment
@Trojanian: Deltics рассказал вам, как предварительно установить емкость: capacity := file size / average line length. Например: MyStringList.Capacity := Reader.BaseStream.Size div AverageLineLength; Вы должны предоставить значение для AverageLineLength на основе того, как на самом деле выглядят ваши данные. - person Remy Lebeau; 19.11.2014
comment
@RemyLebeau: Спасибо - очень связно. Я узнал из этого поста. :-) - person Trojanian; 20.11.2014
comment
FWIW, потоковый ридер ужасающе неэффективен. Каждый раз, когда вы что-то потребляете, оставшаяся часть буфера перемещается вниз с помощью TStringBuilder.Remove. Это даже приводит к перераспределению буфера для уменьшения его емкости. Производительность потокового чтения ухудшается по мере увеличения размера буфера. Я не могу поверить, насколько ужасно плоха реализация. - person David Heffernan; 24.03.2015