Почему избыточная память для строк в Delphi? - PullRequest
9 голосов
/ 23 ноября 2008

Я читаю в большом текстовом файле с 1,4 миллионами строк размером 24 МБ (в среднем 17 символов в строке).

Я использую Delphi 2009, файл ANSI, но при чтении он конвертируется в Unicode, поэтому, если честно, можно сказать, что размер преобразованного текста составляет 48 МБ.

(Изменить: я нашел гораздо более простой пример ...)

Я загружаю этот текст в простой StringList:

  AllLines := TStringList.Create;
  AllLines.LoadFromFile(Filename);

Я обнаружил, что строки данных, похоже, занимают гораздо больше памяти, чем их 48 МБ.

На самом деле они используют 155 МБ памяти.

Я не возражаю против использования Delphi 48 МБ или даже целых 60 МБ с учетом некоторых накладных расходов на управление памятью. Но 155 МБ кажется чрезмерным.

Это не ошибка StringList. Ранее я пытался загрузить строки в структуру записи, и я получил тот же результат (160 МБ).

Я не вижу и не понимаю, что может быть причиной того, что Delphi или менеджер памяти FastMM используют в 3 раза больше памяти, необходимой для хранения строк. Распределение кучи не может быть настолько неэффективным, не так ли?

Я отладил это и исследовал, насколько смог. Будем весьма благодарны за любые идеи относительно того, почему это может происходить, или за идеи, которые могут помочь мне сократить избыточное использование.

Примечание: я использую этот "меньший" файл в качестве примера. Я действительно пытаюсь загрузить файл объемом 320 МБ, но Delphi запрашивает более 2 ГБ ОЗУ и не хватает памяти из-за этого лишнего требования к строке.

Addenum: Марко Канту только что выпустил Белую книгу по Delphi и Unicode . Delphi 2009 увеличил накладные расходы на строку с 8 байтов до 12 байтов (плюс еще 4 для фактического указателя на строку). Дополнительные 16 байтов на строку 17x2 = 34 байта добавляют почти 50%. Но я вижу более 200% накладных расходов. Какими могут быть дополнительные 150%?


Успех !! Спасибо всем вам за ваши предложения. Вы все заставили меня задуматься. Но я должен отдать должное Яну Гойваэртсу за ответ, так как он спросил:

... почему вы используете TStringList? Должен ли файл действительно храниться в памяти в виде отдельных строк?

Это привело меня к решению, что вместо загрузки файла размером 24 МБ в виде StringList объемом 1,4 миллиона я могу сгруппировать строки в естественные группы, о которых знает моя программа. Это привело к загрузке 127 000 строк в список строк.

Теперь в каждой строке в среднем 190 символов вместо 17. В каждой строке StringList накладные расходы такие же, но теперь строк намного меньше.

Когда я применяю это к файлу 320 МБ, он больше не исчерпывает память и теперь загружает менее 1 ГБ ОЗУ. (И загрузка занимает всего около 10 секунд, что очень хорошо!)

Для анализа сгруппированных строк потребуется немного больше обработки, но это не должно быть заметно при обработке каждой группы в реальном времени.

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

Ответы [ 8 ]

9 голосов
/ 23 ноября 2008

Вы попросили меня лично ответить на ваш вопрос здесь. Я не знаю точную причину, почему вы видите такое высокое использование памяти, но вы должны помнить, что TStringList делает гораздо больше, чем просто загрузка вашего файла. Каждый из этих шагов требует памяти, которая может привести к фрагментации памяти. TStringList должен загрузить ваш файл в память, преобразовать его из Ansi в Unicode, разделить его на одну строку для каждой строки и объединить эти строки в массив, который будет перераспределен много раз.

Мой вопрос к вам, почему вы используете TStringList? Должен ли файл храниться в памяти отдельными строками? Собираетесь ли вы изменить файл в памяти или просто отобразить его части? Хранение файла в памяти в виде одного большого фрагмента и сканирование всего этого с помощью регулярных выражений, соответствующих требуемым фрагментам, будет более эффективным с точки зрения памяти, чем хранение отдельных строк.

Кроме того, должен ли весь файл быть преобразован в Unicode? Пока ваше приложение - Unicode, ваш файл - Ansi. Моя общая рекомендация заключается в том, чтобы преобразовать входные данные Ansi в Unicode как можно скорее, потому что это экономит циклы процессора. Но когда у вас есть 320 МБ данных Ansi, которые останутся данными Ansi, потребление памяти станет узким местом. Попробуйте сохранить файл как Ansi в памяти и конвертировать только те части, которые вы будете отображать для пользователя, как Ansi.

Если файл размером 320 МБ - это не файл данных, из которого вы извлекаете определенную информацию, а набор данных, который вы хотите изменить, рассмотрите возможность его преобразования в реляционную базу данных и позвольте механизму базы данных беспокоиться о том, как управлять огромными данными. набор данных с ограниченной оперативной памятью.

8 голосов
/ 23 ноября 2008

Что если вы сделали свою оригинальную запись с использованием AnsiString? Это сразу разрезает его пополам? Если Delphi по умолчанию использует UnicodeString, это не означает, что вы должны его использовать.

Кроме того, если вы точно знаете длину каждой строки (в пределах одного или двух символов), то может быть лучше использовать даже короткие строки и сбрить еще несколько байтов.

Мне любопытно, может быть, есть лучший способ выполнить то, что вы пытаетесь сделать. Загрузка 320 МБ текста в память может быть не лучшим решением, даже если вы можете получить его всего за 320 МБ

.
6 голосов
/ 23 ноября 2008

Я использую Delphi 2009, файл ANSI, но при чтении он конвертируется в Unicode, поэтому, если честно, можно сказать, что размер преобразованного текста составляет 48 МБ.

Извините, но я совсем этого не понимаю. Если вам нужно, чтобы ваша программа была в Unicode, то файл «ANSI» (в нем должен быть какой-то набор символов, например, WIN1252 или ISO8859_1) - это не то, что нужно. Сначала я бы преобразовал его в UTF8. Если файл не содержит символов> = 128, это ничего не изменит (даже будет того же размера), но вы готовы к будущему.

Теперь вы можете загрузить его в строки UTF8, что не удвоит потребление памяти. Преобразование на лету нескольких строк, которые одновременно могут быть видны на экране, в строку Delphi Unicode будет медленнее, но, учитывая меньший объем памяти, ваша программа будет работать намного лучше в системах с небольшим (бесплатным) память.

Теперь, если ваша программа по-прежнему использует слишком много памяти с TStringList, вы всегда можете использовать TStrings или даже IStrings в вашей программе и написать класс, который реализует IStrings или наследует TStrings и не сохраняет все строки в памяти. Несколько идей, которые приходят на ум:

  1. Считайте файл в TMemoryStream и сохраните массив указателей на первые символы строк. Возврат строки очень прост, вам нужно только вернуть правильную строку между началом строки и началом следующей, с обрезанными CR и NL.

  2. Если это все еще занимает слишком много памяти, замените TMemoryStream на TFileStream и не сохраняйте массив указателей на символы, но начинается массив смещений файлов для строки.

  3. Вы также можете использовать функции Windows API для файлов, отображаемых в память. Это позволяет вам работать с адресами памяти, а не смещениями файлов, но не потребляет столько памяти, сколько первая идея.

4 голосов
/ 23 ноября 2008

По умолчанию Delphi 2009 TStringList считывает файл как ANSI, если только нет метки порядка байтов, чтобы идентифицировать файл как что-то еще, или если вы предоставляете кодировку в качестве необязательного второго параметра LoadFromFile.

Так что, если вы видите, что TStringList занимает больше памяти, чем вы думаете, тогда происходит что-то еще.

3 голосов
/ 23 ноября 2008

Вы случайно не компилируете программу с источниками FastMM из sourceforge и с определенным FullDebugMode? В этом случае FastMM на самом деле не освобождает неиспользуемые блоки памяти, что объясняет проблему.

1 голос
/ 24 ноября 2008

Частично это может быть алгоритм распределения блоков. По мере того как ваш список увеличивается, он начинает увеличивать объем памяти, выделяемой для каждого блока. Я не смотрел на это долгое время, но я считаю, что это происходит как удвоение количества последнего, выделенного каждый раз, когда ему не хватает памяти. Когда вы начинаете работать с такими большими списками, ваши ассигнования также намного больше, чем вам в конечном итоге нужно.

Edit- Как отметил Лекесслер, это увеличение на самом деле составляет всего 25%, но все же его следует рассматривать как часть проблемы. если вы находитесь за пределами переломного момента, в списке может быть огромный блок памяти, который не используется.

1 голос
/ 23 ноября 2008

Вы полагаетесь на Windows, чтобы сказать вам, сколько памяти использует программа? Он печально известен завышением объема памяти, используемой приложением Delphi.

Однако я вижу, что в вашем коде много дополнительного использования памяти.

Ваша структура записи составляет 20 байт - если в каждой строке имеется одна такая запись, вы просматриваете больше данных для записей, чем для текста.

Кроме того, строка имеет 4-байтовую служебную информацию - еще 25%.

Я полагаю, что в обработке кучи в Delphi есть определенная степень детализации распределения, но я не помню, что это такое. Даже при 8 байтах (два указателя на связанный список свободных блоков) вы смотрите еще на 25%.

Обратите внимание, что мы уже выросли более чем на 150%.

0 голосов
/ 24 ноября 2008

Почему вы загружаете этот объем данных в TStringList? Сам список будет иметь некоторые накладные расходы. Возможно, TTextReader мог бы вам помочь.

...