Почему в Delphi слишком много памяти для строк?

Я читаю большой текстовый файл с 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%?


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

...why are you using TStringList? Must the file really be stored in memory as separate lines?

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

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

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

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

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

Как вы измеряете требуемую память? Надеюсь, не с столбцом «Использование памяти» в диспетчере задач. Это не измерение того, что вы могли бы подумать.

Lars Truijens 23.11.2008 15:06

Для измерения памяти я использую GlobalMemoryStatusEx. См .: msdn.microsoft.com/en-us/library/aa366589(VS.85).aspx

lkessler 24.11.2008 01:28

Вы должны проверить, сколько памяти действительно используется в Delphi. Delphi MM будет перераспределять большие блоки, которые он получает от ОС, и передавать их ОС только тогда, когда это возможно (фрагментация и тому подобное может отрицать это), поэтому то, что видит Windows и что делает Delphi, может отличаться. Если вы используете полную библиотеку FastMM, доступную на Sourceforge, у нее есть средства для запроса распределения MM, что дает вам более глубокое представление о том, что происходит. В противном случае вы можете использовать профилировщик памяти (например, AQTime), чтобы проверить его и посмотреть, какая выделенная память, когда и почему.

Mad Hatter 12.04.2012 14:27
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
3
5 203
8
Перейти к ответу Данный вопрос помечен как решенный

Ответы 8

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

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

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

Кроме того, строка имеет 4 байта накладных расходов - еще 25%.

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

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

Накладные расходы UnicodeString составляют четыре байта для длины, четыре байта для счетчика ссылок и два байта для нулевого значения в конце.

Rob Kennedy 23.11.2008 08:55

В моем предыдущем примере с записями я специально заявил, что сравниваю загрузку записи и назначение строки для загрузки записи без назначения строки. Следовательно, разница была связана только с строкой, а не с 20 байтами в записи.

lkessler 23.11.2008 10:36

Что, если в исходной записи используется AnsiString? Это сразу же разрубит пополам? Тот факт, что Delphi по умолчанию использует UnicodeString, не означает, что вы должны его использовать.

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

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

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

lkessler 23.11.2008 10:42

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

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

Спасибо, Ник. Хммм ... Не представляю, что еще происходит. Мой пример довольно прост.

lkessler 24.11.2008 08:39

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

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

lkessler 24.11.2008 01:38
Ответ принят как подходящий

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

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

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

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

Спасибо, Ян, за твои идеи, и это дает мне больше поводов для размышлений. Ваше предложение «чанка» заставляет меня попробовать загрузить группы строк, которые в среднем составляют около 150 символов на группу, а не 17 символов на строку. Программное обеспечение для генеалогии должно быть Unicode.

lkessler 24.11.2008 01:36

Конечно, ваше программное обеспечение должно быть Unicode. Но это не значит, что вам нужно хранить в памяти 320 МБ данных в Юникоде, если источник не Юникод.

Jan Goyvaerts 24.11.2008 19:27

I using Delphi 2009 and the file is ANSI but gets converted to Unicode upon reading, so fairly you can say the text once converted is 48 MB in size.

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

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

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

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

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

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

Ваши 3 идеи хороши. Но преобразование в UTF8 неэффективно и неправильно в Delphi 2009. Я должен либо сохранить его в ANSI и преобразовать в Unicode, когда мне нужно, либо поглотить дополнительные 24 МБ (что я готов сделать) и преобразовать в Unicode для программы. использовать.

lkessler 25.11.2008 01:35

Извините, но я не согласен. UTF8 - это правильный формат для хранения и обмена данными, и, поскольку ввод-вывод намного медленнее, чем обработка ЦП, он должен дать вам не только файлы на диске меньшего размера, но и лучшую производительность. Независимо от формата внутренней строки, я всегда буду использовать UTF8 для файлов данных.

mghie 25.11.2008 01:48

Файлы данных часто имеют гораздо большую ценность, чем программный код, поэтому оптимизация для конкретной среды программирования неверна. Их формат должен быть выразительным, но эффективным, желательно стандартизированным. UTF8 дает вам все это и является наиболее распространенным за пределами Windows. Что не нравится?

mghie 25.11.2008 01:54

UTF-8 - правильный формат. Данные если и только если лучше всего хранить в UTF-8. Если вы собираетесь хранить, скажем, китайские данные в UTF-8, вы получаете худшее хранилище и ввод-вывод, чем хранение и передача их в UTF-16. UTF-8 подходит, если у вас большая часть текста на английском или латинском языках. Для кириллицы, греческого, иврита и арабского языка также требуется два байта в UTF-8, поэтому нет выигрыша по сравнению с UTF-16. Это напомнило мне людей, которые говорили, что ASCII7 - это подходящее хранилище для каждого текста, и вам действительно не нужны диакритические знаки ... мир немного больше, чем Западная Европа.

Mad Hatter 12.04.2012 14:23

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

TTextReader помогает только анализировать ввод. Я уже сам это делаю очень эффективно. Затем мне нужно поместить где-нибудь проанализированные строки. Первоначально я пытался использовать записи и обнаружил эту проблему с использованием памяти. Затем я обнаружил ту же проблему в TStringList и оставил ее в вопросе в качестве более простого примера.

lkessler 24.11.2008 23:10

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

РЕДАКТИРОВАТЬ- Как указал Икесслер, на самом деле это увеличение составляет всего 25%, но его все же следует рассматривать как часть проблемы. Если вы находитесь чуть выше критической точки, то для списка может быть выделен огромный блок памяти, который не используется.

Это было хорошее предложение, но TStringList.Grow каждый раз увеличивает размер только на 25%. Таким образом, наибольшие накладные расходы составляют 25%.

lkessler 25.11.2008 09:46

Другие вопросы по теме