TStringList, динамический массив или связанный список в Delphi?

У меня есть выбор.

У меня есть ряд уже упорядоченных строк, которые мне нужно сохранить и получить. Похоже, я могу выбирать между использованием:

  • TStringList
  • Динамический массив строк и
  • Связанный список строк (односвязный)

    и Алан в своем комментарии предложил мне также добавить к выборам:

  • TList<string>

В каких обстоятельствах каждый из них лучше других?

Что лучше для небольших списков (менее 10 элементов)?

Что лучше для больших списков (более 1000 элементов)?

Что лучше для огромных списков (более 1 000 000 предметов)?

Что лучше всего свести к минимуму использование памяти?

Что лучше всего минимизировать время загрузки для добавления дополнительных элементов в конец?

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

На этой основе (или любых других), какая структура данных предпочтительнее?

Для справки, я использую Delphi 2009.


Димитрий в комментарии сказал:

Опишите свою задачу и шаблон доступа к данным, тогда вы сможете дать вам точный ответ

Хорошо. У меня есть генеалогическая программа с большим количеством данных.

Для каждого человека у меня есть несколько событий и атрибутов. Я храню их как короткие текстовые строки, но их много для каждого человека, от 0 до нескольких сотен. И у меня тысячи людей. Мне не нужен случайный доступ к ним. Мне нужно, чтобы они были связаны как число строк в известном порядке, прикрепленном к каждому человеку. Это мой случай с тысячами "небольших списков". Они занимают время для загрузки и использования памяти и требуют времени для доступа, если мне все они нужны (например, для экспорта всего сгенерированного отчета).

Затем у меня есть несколько более крупных списков, например. все имена разделов моего "виртуального" дерева, которые могут иметь сотни тысяч имен. Опять мне нужен только список, к которому я могу получить доступ по индексу. Они сохраняются отдельно от древовидной структуры для повышения эффективности, а treeview извлекает их только по мере необходимости. Это занимает некоторое время, чтобы загрузить и очень дорого для памяти для моей программы. Но мне не нужно беспокоиться о времени доступа, потому что только несколько из них получают доступ за раз.

Надеюсь, это даст вам представление о том, чего я пытаюсь выполнить.

p.s. Я поставил много вопросов об оптимизации Delphi здесь, в StackOverflow. Моя программа считывает файлы размером 25 МБ со 100 000 человек и создает для них структуры данных, отчет и древовидную структуру за 8 секунд, но для этого используется 175 МБ ОЗУ. Я работаю над тем, чтобы уменьшить это, потому что я нацелен на загрузку файлов с несколькими миллионами человек в 32-разрядной Windows.


Я только что нашел отличные рекомендации по оптимизации TList в этом вопросе StackOverflow: Есть ли более быстрая реализация TList?

Ответ 1

Если у вас есть особые потребности, TStringList трудно превзойти, поскольку он предоставляет интерфейс TStrings, который многие компоненты могут использовать напрямую. С помощью TStringList.Sorted := True будет использоваться двоичный поиск, что означает, что поиск будет очень быстрым. Вы также получаете бесплатное отображение объектов, каждый элемент также может быть связан с указателем, и вы получаете все существующие методы для маршаллинга, потоковых интерфейсов, запятой, разделителем текста и т.д.

С другой стороны, для особых нужд, если вам нужно делать много вставок и удалений, то лучше подходит к более близкому списку. Но затем поиск становится медленнее, и это редкая коллекция строк, которая действительно не нуждается в поиске. В таких ситуациях некоторый тип хэша часто используется там, где создается хэш из, скажем, первых двух байтов строки (preallocate массив длиной 65536, а первые 2 байта строки преобразуются непосредственно в хэш индекс в этом диапазоне), а затем в этом хэш-местоположении связанный список сохраняется с каждым ключом элемента, состоящим из оставшихся байтов в строках (для сохранения пробела --- хэш-индекс уже содержит первые два байта). Затем начальный поиск хэша - O (1), а последующие вставки и удаления связаны-list-fast. Это компромисс, который можно манипулировать, и рычаги должны быть ясными.

Ответ 2

  • TStringList. Плюсы: расширенная функциональность, позволяющая динамически расти, сортировать, сохранять, загружать, искать и т.д. Минусы: при большом количестве доступа к элементам по индексу Strings [Index] вводит ощутимую производительность (несколько процентов), сравнивая для доступа к массиву, служебных данных памяти для каждой ячейки элемента.

  • Динамический массив строк. Плюсы: сочетает способность динамически расти, как TStrings, с самым быстрым доступом по индексу, минимальное использование памяти у других. Минусы: ограниченная стандартная функциональность "список строк".

  • Связанный список строк (односвязный). Плюсы: линейная скорость добавления элемента к концу списка. Минусы: самый медленный доступ по индексу и поиску, ограниченная стандартная функциональность "список строк", служебные данные памяти для указателя "следующий элемент", служебные служебные данные для каждого распределения памяти элемента.

  • TList < строкa > . Как указано выше.

  • TStringBuilder. У меня нет хорошей идеи, как использовать TStringBuilder в качестве хранилища для нескольких строк.

На самом деле существует гораздо больше подходов:

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

Лучший подход будет зависеть от задачи.

Это лучше всего подходит для небольших списков (под 10 элементов)?

Любой, может быть даже статическим массивом с общей переменной count элементов.

Что лучше для больших списков (более 1000 наименований)? Что лучше для огромных списков (более 1 000 000 предметов)?

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

Что лучше всего свести к минимуму использование памяти?

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

Что лучше всего минимизировать время загрузки для добавления дополнительных элементов в конец?

Динамический массив

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

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

динамический массив.

На этой основе (или любых других), какая структура данных предпочтительнее?

Для какой задачи?

Ответ 3

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

Выполняйте математику - вы в настоящее время загружаете файл размером 25 МБ с примерно 100 000 человек, что заставляет ваше приложение потреблять 175 МБ памяти. Если вы хотите загрузить файлы с несколькими миллионами человек, вы можете оценить, что без резких изменений в вашей программе вам потребуется умножить ваши потребности в памяти на n * 10. Нет никакого способа сделать это в 32-битном процессе, сохраняя все в памяти так, как вы сейчас делаете.

В основном у вас есть два варианта:

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

  • Хранить все в памяти, но максимально экономично. Пока нет 64-битного Delphi, это должно позволить нескольким миллионам человек, в зависимости от того, сколько данных будет для каждого человека. Перекомпиляция этого для 64-битного кода также устранит этот предел.

Если вы переходите к второму варианту, вам нужно значительно снизить потребление памяти:

  • Используйте интернирование строк. Каждый загруженный элемент данных в вашей программе, содержащий одни и те же данные, но содержащийся в разных строках, представляет собой в основном потерянную память. Я понимаю, что ваша программа - это средство просмотра, а не редактор, поэтому вам, возможно, удастся просто добавить строки в ваш пул интернированных строк. Выполнение интерполяции строк с помощью миллионов строк по-прежнему затруднено, "Оптимизация потребления памяти со строковыми пулами" в блогах в блоге SmartInspect может дать вам некоторые хорошие идеи. Эти ребята регулярно занимаются огромными файлами данных и вынуждены работать с теми же ограничениями, с которыми вы сталкиваетесь.
    Это также должно соединить этот ответ с вашим вопросом - если вы используете интернирование строк, вам не нужно будет хранить списки строк в ваших структурах данных, но списки индексов пула строк.
    Также может быть полезно использовать несколько пулов строк, например, для имен, но другое для таких мест, как города или страны. Это должно ускорить вставку в пулы.

  • Используйте строковое кодирование, которое дает наименьшее представление в памяти. Сохранение всего как родной строки Windows Unicode, вероятно, потребует гораздо больше места, чем сохранение строк в UTF-8, если вы не будете регулярно заниматься строками, которые содержат в основном символы, которым требуется три или более байтов в кодировке UTF-8.
    Из-за необходимого преобразования набора символов вашей программе потребуется больше циклов процессора для отображения строк, но с таким количеством данных это достойный компромисс, поскольку доступ к памяти будет узким местом, а меньший размер данных помогает уменьшить нагрузку на доступ к памяти.

Ответ 4

TStringList хранит массив указателей на записи (string, TObject).

TList хранит массив указателей.

TStringBuilder не может хранить коллекцию строк. Он похож на .NET StringBuilder и должен использоваться только для конкатенации (многих) строк.

Изменение размеров динамических массивов происходит медленно, поэтому даже не рассматривайте его как вариант.

Я бы использовал общий набор TList<string> Delphi во всех ваших сценариях. Он хранит массив строк (не строковые указатели). Он должен иметь более быстрый доступ во всех случаях из-за отсутствия (un) бокса.

Возможно, вам удастся найти или реализовать немного улучшенное решение для связанных списков, если вам нужен только последовательный доступ. См. Алгоритмы и структуры данных Delphi.

Delphi продвигает свои TList и TList<>. Реализация внутреннего массива сильно оптимизирована, и я никогда не сталкивался с проблемами производительности и памяти при ее использовании. См. Эффективность TList и TStringList

Ответ 5

Один вопрос: как вы запрашиваете: соответствуют ли строки или запрос идентификатору или позиции в списке?

Лучше всего для маленьких # строк:

Все, что делает вашу программу понятной. Чтение программы очень важно, и вы должны жертвовать ею только в реальных горячих точках в своем приложении для скорости.

Наилучшее для памяти (если это наибольшее ограничение) и время загрузки:

Сохраняйте все строки в одном буфере памяти (или файле с отображением памяти) и сохраняйте указатели только на строки (или смещения). Всякий раз, когда вам нужна строка, вы можете вырезать строку с помощью двух указателей и возвращать ее как строку Delphi. Таким образом, вы избегаете накладных расходов самой строковой структуры (refcount, length int, codepage int и структуры диспетчера памяти для каждого распределения строк.

Это работает только в том случае, если строки являются статическими и не меняются.

TList, TList < > , массив строки и вышеприведенное решение имеют "список" накладных расходов одного указателя на строку. Связанный список имеет накладные расходы как минимум на 2 указателя (один связанный список) или 3 указателя (двойной список). Решение связанного списка не имеет быстрого произвольного доступа, но допускает изменения O (1), в которых другие параметры имеют O (lgN) (с использованием коэффициента изменения размера) или O (N) с использованием фиксированного изменения размера.

Что я буду делать:

Если < 1000 наименований и производительность не имеют особого значения: используйте TStringList или массив dyn, что проще всего для вас. else if static: используйте трюк выше. Это даст вам время запроса O (lgN), наименее используемую память и очень быструю нагрузку (просто gulp в или используйте файл с отображением памяти)

Все упомянутые структуры в вашем вопросе потерпят неудачу при использовании больших объемов строк 1M +, которые должны быть динамически отредактированы в коде. В это время я использовал бы балансное двоичное дерево или хеш-таблицу в зависимости от типа запросов, которые мне нужно выполнить.

Ответ 6

Из вашего описания я не совсем уверен, смогу ли он вписаться в ваш дизайн, но один из способов улучшить использование памяти без огромного снижения производительности - с помощью trie.

Преимущества относительно дерева двоичного поиска

Ниже перечислены основные преимущества попыток двоичных деревьев поиска (BSTs):

  • Поиск ключей быстрее. Поиск ключа длиной m принимает худший случай O (m). A BST выполняет O (log (n)) сравнение ключей, где n - это количество элементов в дереве, потому что поиск зависит от глубины дерево, которое является логарифмическим в количество ключей, если дерево сбалансирован. Следовательно, в худшем случае BST принимает время O (m log n). Более того, в худшем случае log (n) будет приближаться м. Кроме того, простые операции использование во время поиска, например массив индексирование с использованием символа происходит быстро на реальных машинах.

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

  • Пробует сближение длинного префикса, помогая найти ключ используя самый длинный префикс символы уникальны.

Ответ 7

Возможная альтернатива:

Недавно я обнаружил SynBigTable (http://blog.synopse.info/post/2010/03/16/Synopse-Big-Table), который имеет класс TSynBigTableString для хранения больших объемов данных с использованием индекса строки.

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

Проще, чем:

aId: = UTF8String (Формат ('% s.% s', [имя, фамилия]));

bigtable.Add(данные, aId)

и

bigtable.Get(aId, data)

Один улов, индексы должны быть уникальными, а стоимость обновления немного выше (сначала удалите, затем снова вставьте)