Насколько заметна разница в производительности между TList, TObjectList и простым массивом, если ее можно оценить?

* Обобщение:

Пожалуйста, проверьте компетентные комментарии экспертов Delphi. Специально для меня я бы попытался использовать старый TList / TObjectList, как предложил Дэвид, и использовать жесткое приведение и свойство TObjectList.List, как предложил А.Бушез. Буду пробовать TDynArray при рефакторинге в будущем.

=====================================================================

Скажем, у меня есть класс TAtom, как определено в следующем коде. Во время выполнения существует от hundreds до thousands экземпляров TAtom, на данный момент stored in a dynamic array. Во время выполнения мне нужно выполнять простые вычисления с плавающей запятой для TAtom.X/Y/Z всех существующих экземпляров TAtom более 30 раз в секунду.

Теперь мне нужно добавить возможность adding, inserting, deleting экземпляров TAtom во время выполнения. Похоже, что я выбрал (1) запросить большой массив; (2) придерживаться динамического массива и вручную SetLength; (3) перейти на обычный TList; (4) перейти на обычный TObjectList.

Я хочу избежать (1), если в этом нет необходимости, потому что тогда мне придется изменить довольно много сигнатур функций. (2) тоже выглядит не очень хорошо, потому что TList / TObjectList, похоже, создан для этой задачи. Однако, поскольку требуется приведение типов с использованием обычного TList / TObjectList, может ли кто-нибудь прокомментировать возможное снижение производительности? Я имею в виду, что было бы лучше, если бы можно было оценить нагрузку на производительность, прежде чем я переписываю код. Если производительность заметно упадет, могу ли я использовать другие методы?

Кроме того, мне интересно, есть ли разница в производительности между использованием TList и TObjectList?

  TAtom = class
  public
    ElementZ: Integer;
    X, Y, Z: Extended;  
    other variables: other types;
  end;

  TAAtom = array of TAtom;

person SOUser    schedule 18.03.2011    source источник
comment
Я бы посоветовал измерить это и убедиться в этом сам.   -  person Ondrej Kelle    schedule 18.03.2011
comment
@TOndrej: Мне просто интересно, можно ли это оценить для моей ситуации (простая математика с плавающей запятой от сотен до тысяч экземпляров 30 раз в секунду) ...: D Может быть эмпирическое правило?   -  person SOUser    schedule 18.03.2011
comment
@Xichen Li, операции Delete и Insert будут наихудшим случаем для всех TList, TObjectList и динамических массивов; Если вам нужно подумать о производительности, найти способ избежать штрафа, связанного с удалением, окупаемость инвестиций будет значительно выше!   -  person Cosmin Prund    schedule 18.03.2011
comment
@Xichen Li, вам нужен произвольный доступ к любому элементу в массиве? Если последовательного доступа достаточно, вам лучше подойдет скромный связанный список: он обеспечивает O (1) вставку, удаление и добавление.   -  person Cosmin Prund    schedule 18.03.2011
comment
@Cosmin: Большое спасибо за ваши комментарии! Не могли бы вы предложить какие-нибудь правдоподобные способы поднять return on investment в моей ситуации?   -  person SOUser    schedule 18.03.2011
comment
@Xichen Li, мой предыдущий комментарий рекомендует использовать связанный список, потому что он имеет O (1) insert / delete / append. Другие идеи включают в себя не удаление, а замену значений на nil (чтобы вам не нужно было сдвигать буфер) или замену всей структуры данных деревом: дерево могло бы лучше сбалансировать стоимость произвольного доступа со стоимостью вставить и удалить. Конечно, все зависит от вашей загруженности, и только вы знаете, что это такое.   -  person Cosmin Prund    schedule 18.03.2011
comment
@cosmin: Я не особо использую random access. Но я делаю sequential access много. Например, для каждого пакета простых математических вычислений с плавающей запятой мне нужно перебрать экземпляры. Я предполагаю, что структура данных linked list не так хороша, как array в sequential access?   -  person SOUser    schedule 18.03.2011
comment
@Cosmin: Большое спасибо за полезные предложения! Мне нужно немного подумать о них. : D   -  person SOUser    schedule 18.03.2011
comment
Нет, связанный список отлично подходит для последовательного доступа, но не может использоваться для произвольного доступа. Вам нужно полностью отказаться от произвольного доступа или принять действительно высокий штраф за его использование (вам нужно будет пройтись по всему списку, чтобы получить запись по индексу).   -  person Cosmin Prund    schedule 18.03.2011
comment
@Xichen Перед тем, как реализовать связанные списки (более сложные, чем списки или массивы), убедитесь, что у вас действительно проблемы с производительностью. В противном случае вы будете усложнять без всякой пользы.   -  person David Heffernan    schedule 18.03.2011
comment
@David: Большое спасибо за этот совет! :)   -  person SOUser    schedule 18.03.2011
comment
О связанных списках: это очень хороший шаблон, особенно для удаления или вставки, но для огромного количества элементов учитывайте потребление памяти и фрагментацию. При последовательном чтении заранее выделенный массив записей будет работать быстрее, чем любая коллекция отдельных экземпляров класса. У каждого экземпляра класса TAtom будет своя собственная память (вызовы GetMem / FreeMem всегда стоят) и не будут выделяться непрерывно: для последовательного доступа она будет медленнее. Предварительно выделенный массив записей будет оптимизирован для кеш-памяти L1 и L2 ЦП.   -  person Arnaud Bouchez    schedule 18.03.2011
comment
@ A.Bouchez, оптимизация производительности - это искусство балансировки ограничений: конечно, ничто не сравнится с массивом записей для последовательного доступа, но если вам нужно выполнить значительное количество удалений и вставок в большом массиве, вы действительно собираетесь почувствуйте их. ИМХО, выбор лучших структур данных и алгоритмов - это наилучшая возможная оптимизация, не мучительная по относительной скорости динамического массива и TList.   -  person Cosmin Prund    schedule 18.03.2011
comment
@Cosmin Prund Это именно моя точка зрения. Аминь!   -  person Arnaud Bouchez    schedule 18.03.2011
comment
@Cosmin iiuc находит точку вставки / удаления в связанном списке O (n) - stackoverflow.com/questions/840648/   -  person mjn    schedule 18.03.2011


Ответы (8)


Если вы используете Generics.Collections.TObjectList<TAtom> и нет необходимости в кастинге.

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

Пока вы избегаете SetLength(A, Length(A)+1) и выбираете более разумную стратегию распределения, динамические массивы эквивалентны всем TList-подобным классам.

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

Все это несколько умозрительно, и вам действительно нужно измерить - иначе мы можем только догадываться.

person David Heffernan    schedule 18.03.2011
comment
@David: Большое спасибо за уделенное время! Но если использовать старый, обычный TList / TObjectList, можно ли оценить падение производительности? - person SOUser; 18.03.2011
comment
Производительность старого TList / TObjectList будет практически такой же, как и у общих версий. - person David Heffernan; 18.03.2011
comment
@David: Большое спасибо за Old TList/TObjectList performance will be essentially the same as the generic versions. Во-вторых, я понимаю, что вставка более требовательна, но inserting кажется необходимым для undo deleting. Кроме того, большое спасибо за то, что поделились своим опытом! Я полагаю, вы использовали массив записей? Не могли бы вы прокомментировать количество случаев в ваших проблемах? - person SOUser; 18.03.2011
comment
Предварительное выделение - это уловка, которую нужно использовать с внешней переменной Count: думайте о длине динамического массива как о его емкости, а не о количестве элементов. - person Arnaud Bouchez; 18.03.2011
comment
@ A.Bouchez Предварительное выделение памяти не помогает, когда вашим массивам требуются очень большие непрерывные блоки памяти и они ограничены 32-битным адресным пространством. Тогда вам нужно перераспределение. - person David Heffernan; 18.03.2011
comment
@Xichen Трудно дать общий совет, не зная своей проблемы. Единственная область, где у вас есть место для маневра, - это вставка и удаление в / из середины списка. Вам действительно нужно это делать? В любом случае, сколько вставок вы выполняете при обходе списка? Если это небольшое число, у вас не будет проблем с производительностью. - person David Heffernan; 18.03.2011
comment
@David Из того, что Xichen Li написал выше (100 или 1000 элементов), данные окончательно поместятся в 32-битное адресное пространство. ;) - person Arnaud Bouchez; 18.03.2011
comment
@David: Я выполняю только простые вычисления с плавающей запятой для существующих экземпляров за прогулку. inserting или deleting выполняется только по запросу пользователей. Например, можно выбрать группу атомов и удалить их, или можно добавить группу атомов в существующую молекулу (набор атомов). Как вы говорите, большую часть времени это небольшое число. В этом отношении, если переключение на TList / TObjectList дает почти такую ​​же скорость для простого sequential доступа, что и при использовании динамического массива, TList / TObjectList кажется совершенно нормальным. - person SOUser; 18.03.2011
comment
@ A.Bouchez Поскольку это всего лишь 100 или 1000 мелких элементов, содержащих несколько значений с плавающей запятой, и один такой список в приложении, я, вероятно, был бы склонен использовать Generics.Collections.TList<TMyRecord>. Это запись с методами, а не с классом. На самом деле, непрерывное выделение - это то, что вам здесь нужно. - person David Heffernan; 18.03.2011
comment
@Xichen Если вставка / удаление выполняется только по запросу пользователя, тогда определенно массив / список (на самом деле то же самое), а не связанный список. И если производительность критична, воспользуйтесь советом @A.Bouchez и поместите его в запись типа значения, чтобы получить наилучшее использование кеша. - person David Heffernan; 18.03.2011

Могу я добавить еще один вариант в ваш список?

Если вы не используете какую-либо функцию наследования для данных в TAtom, вы можете использовать record вместо class. Каждый экземпляр класса должен быть выделен в памяти, заполнен нулями и инициализирован индивидуально. Getmem/Freemem всегда стоит, а фрагментация памяти будет увеличиваться.

Предварительно выделенный динамический array of record будет быстрее, чем отдельные экземпляры класса для добавления. И данные лучше подойдут для кэша L1 / L2 процессора.

Для вставки и удаления массив таких записей будет медленнее, чем TList, если у вас огромное количество элементов, потому что будет больше данных для удаления / вставки (оба TList/TObjectList поддерживают только список указателей). Для еще более быстрой вставки / удаления лучше использовать связанный список.

В механизме TList/TObjectList есть некоторые накладные расходы из-за внутреннего уведомления. механизм И свойство GetItem() могло бы быть немного медленнее (из-за проверки диапазона), чем использование непосредственно динамического массива.

Но с нашей оболочкой TDynArray вы можете придерживаться динамического массива и при этом иметь хорошая производительность, функции предварительного распределения и TList-подобные методы. И даже больше доступных методов, таких как SaveToStream, Slice, Reverse, сортировка по внешним индексам и тому подобное ...

type
  TAtom = record // could be 'packed record' to save memory (but loose perf)
    ElementZ: Integer;
    X, Y, Z: Extended;  
    other variables: other types;
    // TDynArray also handle complex (e.g. string) types here
  end;
  TAtoms = array of TAtom;

var Atom: TAtom;
    AtomArray: TAtoms;
    AtomCount: integer;
    Atoms: TDynArray;
begin
  Atoms.Init(TypeInfo(TAtoms),AtomArray,@AtomCount);
  Atoms.Capacity := 10000; // pre-allocate array = same as SetLength(AtomArray,10000)
  for i := 1 to 10000 do begin
    A.ElementZ := Random(1000);
    A.X := Random;
    A.Y := Ramdom;
    A.Z := Random;
    // set other fields
    Atoms.Add(A); // fast adding of A properties
  end;
  // you have TList-like methods for your dynamic array
  Atoms.Delete(500); // delete 500th item
  A.ElementZ := 5000;
  Atoms.Insert(500,A); // insert A values at 500th index
  assert(Atoms.Count=10000);
  assert(AtomCount=10000); // same as Atoms.Count
  Atoms.Compare := SortDynArrayInteger;
  Atoms.Sort; // will sort by 1st Integer value = ElementZ
  for i := 1 to Atoms.Count-1 do // or AtomCount-1
    // you have still direct access to AtomArray[]
    // -> this is even the fastest access to the data
    assert(AtomArray[i].ElementZ >=AtomArray[i-1].ElementZ )
  Atoms.SaveToStream(aStream); // will also save any string content
  Atoms.Reverse; // reverse all items order
  Atoms.Clear;
  // faster adding will be done with direct access to the dynamic array
  Atom.Count := 10000; // allocate memory for 10000 items
  for i := 0 to 10000-1 do
  with AtomArray[i] do
  begin
    ElementZ := Random(2000);
    X := Random;
    Y := Random;
    Z := Random;
  end;
  Atoms.Sort; // TDynArray knows about the data just created
end; // no need to have any try...finally ..Free block

Работает с Delphi 6 до XE.

С более новой версией Delphi, поддерживающей дженерики, вам лучше пойти в этом направлении.

person Arnaud Bouchez    schedule 18.03.2011
comment
Большое спасибо за ваше предложение! Я постараюсь сначала понять ваш код, прежде чем продолжать комментировать. : D - person SOUser; 18.03.2011
comment
Не стесняйтесь задавать вопросы здесь или на нашем форуме, если вам нужна информация. - person Arnaud Bouchez; 18.03.2011

Однако, поскольку требуется приведение типов с использованием обычного TList / TObjectList, может ли кто-нибудь прокомментировать возможное снижение производительности?

Если вы наберете cast, используя форму

List[I] as TAtom

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

TAtom(List[I])

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

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

person Ken Bourassa    schedule 18.03.2011
comment
Вы правы: приведение типов выполняется во время компиляции. Если вам нужно еще более быстрое выполнение (без вызова Get методов), вы можете написать TAtom(List.List[I]). Таким образом вы напрямую отобразите список внутренних указателей, чтобы код был максимально быстрым (всего несколько кодов операций asm). Но вы должны быть уверены, что переменная I верна (например, for I := 0 to List.Count-1 do), потому что в этом случае проверки диапазона не будет. - person Arnaud Bouchez; 18.03.2011
comment
Большое спасибо за ваш комментарий! Добавлена ​​еще одна ссылка на жесткое приведение по сравнению с исходным на случай, если другие сочтут это полезным. delphi.about.com/od/delphitips2007/qt/is_as_cast.htm - person SOUser; 18.03.2011
comment
@ A.Bouchez: Приятно это знать! Принимая TForceList в эта страница стекового потока, например: Если у меня var tmpList: TForceList;, я думаю, мне следует использовать TForce(tmpList.Items[I] вместо TForce(tmpList.Objects[I]), чтобы понять, что вы имели в виду? Не могли бы вы прокомментировать? - person SOUser; 18.03.2011
comment
TForceList.GetObject вызовет TForceList.Get, поэтому TForce(tmpList.Objects[I]) медленнее, чем TForce(tmpList.Items[I]). Быстрее будет TForce(tmpList.List[I]) (никакого вызова метода, только быстрая арифметика указателя в asm). - person Arnaud Bouchez; 19.03.2011
comment
@Xichen Li о приведении типов, как указано на вашей цитируемой странице SO: позаботьтесь о том, чтобы указать указатель или TObject на NativeInt вместо Integer, чтобы быть готовым к 64-битному компилятору Delphi (который должен быть выпущен в ближайшее время). Пусть наш код подготовлен для этого! - person Arnaud Bouchez; 19.03.2011

Поскольку TObjectList является прямым потомком TList, производительность будет очень близка.

person philnext    schedule 18.03.2011

Первый вопрос: мы говорим о Classes.TList и Contnrs.TObjectList или о Generics.Collections.TList соответственно Generics.Collections.TObjectList?

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


Если мы говорим о более старых TList и TObjectList, то нам нужно сравнивать только TList с эквивалентом динамического массива, поскольку TObjectList является потомком TList, поэтому он наследует все его характеристики производительности. TList использует блок памяти, выделенный с помощью ReallocMem. Внутренне динамический массив делает то же самое, поэтому существенной разницы быть не должно!

Вывод

Если между ними есть какая-то разница в производительности, вероятно, потому, что наивное использование динамического массива использует ужасный SetLength(A, Length(A)+1), в то время как лучшая реализация во всех предоставленных Delphi контейнерах предварительно выделяет память в более крупных частях. При правильном коде между этими альтернативами не должно быть значительной разницы!

person Cosmin Prund    schedule 18.03.2011
comment
Большое спасибо за полезные комментарии! Сейчас я рассматриваю старый, обычный TList / TObjectList. Приятно знать ..., so there shouldn't be a significant difference! и ..., while the better implementation in all Delphi-provided containers pre-allocate memory in larger chunks! - person SOUser; 18.03.2011

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

TList и TObjectList, вероятно, быстрее, чем динамический массив при добавлении, вставке и удалении, потому что динамический массив постоянно должен перераспределяться. Реализация TList и TObjectList этого не делает.

person The_Fox    schedule 18.03.2011
comment
Спасибо за ваше время! Мне просто интересно, можно ли это оценить для моей ситуации (простая математика с плавающей запятой от сотен до тысяч экземпляров 30 раз в секунду). - person SOUser; 18.03.2011
comment
Динамический массив экземпляров класса TAtom будет иметь те же временные характеристики, что и TList из TObjectList, потому что все они используют slow move () при вставке и удалении. Для добавления динамический массив выполняется так же быстро, как TList, если вы предварительно выделяете память, то есть используете SetLength () для установки емкости, а затем используйте внешнюю целочисленную переменную для Count. TList и TObjectList могут быть даже немного медленнее, чем динамический массив из-за внутреннего механизма уведомления. - person Arnaud Bouchez; 18.03.2011
comment
@ A.Bouchez: Я говорю о простом динамическом массиве, а не о классе, который внутренне использует динамический массив. И да, возможно, удаление происходит медленнее, но я думаю, что TList / TObjectList быстрее со случайным удалением. Но опять же, вы должны просто измерить это. - person The_Fox; 18.03.2011
comment
TList / TObjectList переместит () все указатели для случайного удаления (а также при вставке). Таким образом, это будет такая же скорость, как и при ручном удалении динамического массива. Если вы говорили о нашем TDynArray, обратите внимание, что это не класс, который внутренне использует динамический массив, а оболочка для доступа к динамическому массиву. Данные хранятся не в TDynArray, а в динамическом массиве. TDynArray просто предоставляет общие методы для доступа к данным массива благодаря быстрой информации RTTI, доступной со времени первой версии Delphi. - person Arnaud Bouchez; 18.03.2011
comment
@ A.Bouchez: Я прочитал ваш ответ и прочитал pre-allocation features. Итак, я подумал, что вы говорите о классе, в котором Вместимость ‹› Считается. А для этого вам нужно хранить данные. Но с емкостью вы просто имеете в виду длину динамического массива. - person The_Fox; 18.03.2011

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

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

В конце концов, стоимость арифметических операций, выполняемых на TAtom, должна преобладать во времени выполнения, поэтому выбор dynarray или TListXXX не имеет значения.

person Apalala    schedule 18.03.2011
comment
@Спасибо за ваши комментарии! На самом деле я так много делаю: If the sequence is updated infrequently after created.... - person SOUser; 18.03.2011
comment
Извините за опечатку! Я имел ввиду, что последовательность обновляется frequently после ее создания. - person SOUser; 18.03.2011
comment
@Xichen, если у вас частые обновления, то непременно используйте оптимизированные реализации в стандартной библиотеке. - person Apalala; 25.03.2011

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

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

  • связанный список, если вам не нужен доступ к атомам по индексу
  • дерево, если вы использовали его для пространственного раздела ваших атомов, вы могли бы также использовать этот раздел для хранения ваших атомов, а не массива
  • разрешение элементов undefined / nil в вашем массиве / списке и поддержание стека элементов undefined / nil и индекса, если вам нужен отсортированный список (потенциально самое высокопроизводительное решение, но также, вероятно, наиболее сложное с точки зрения эффективности )
person Eric Grange    schedule 18.03.2011
comment
Просто любопытно, вы читали комментарии к вопросу или уже предоставленные ответы? Потому что вы, очевидно, обобщаете эту информацию. Действительно похоже на плагиат - person Cosmin Prund; 18.03.2011
comment
@Cosmin Prund После его работ я сомневаюсь, что Эрик Гранж не знает о структурах данных и просто скопировал его ответ из комментариев. ;) - person Arnaud Bouchez; 18.03.2011
comment
Эрик - Рад видеть, что вы отвечаете на вопросы здесь, о SO. - person Nick Hodges; 18.03.2011
comment
@ A.Bouchez, ну тогда хорошо, что я высказал ему сомнения и не проголосовал против! - person Cosmin Prund; 18.03.2011
comment
@Eric: Большое спасибо за вашу помощь! Я подумаю над вашим предложением the highest performance solution. - person SOUser; 18.03.2011
comment
@ A.Bouchez, @Cosmin: Да, он _ 1_ восстановление DWScript. :) - person SOUser; 18.03.2011
comment
@Cosmin: два первых предложения - это просто введение, и да, в них не так много подробностей о том, что уже было опубликовано. Но я хотел сказать, что производительность списков на основе массивов может не иметь значения, если вставки / удаления значительны, следует исследовать списки, не основанные на массивах, такие как три, которые я упомянул. @others: спасибо;) - person Eric Grange; 21.03.2011