How noticeable is the difference of performance among TList, TObjectList, and plain array, if it could be estimated?
Asked Answered
N

8

9

*Summarization:

Please check the knowledgeable comments from the Delphi experts. Specifically for me, I would try to use old TList/TObjectList as David suggested, and use hard-cast and TObjectList.List property as A.Bouchez suggested. I will try TDynArray when refactoring in future.

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

Say that I have a TAtom class as defined in the following code. There are about hundreds up to thousands of TAtom instances at run time, stored in a dynamic array for now. At run time, I need to do simple float math on TAtom.X/Y/Z of all the existing TAtom instances more than 30 times per second.

Now, I need to add the ability of adding, inserting, deleting of TAtom instances at run time. It seems that my choices are (1) request a big array; (2) stick to dynamic array and manually SetLength; (3) switch to regular TList; (4) switch to regular TObjectList.

I want to avoid (1) unless it is necessary, because I then have to change quite a lot function signatures. (2) looks not good either, because TList/TObjectList seems born for this task. However, because type-casting is needed using the regular TList/TObjectList, could some one comment on the possible performance hit? I mean, it would be best if the performance burden could be estimated before I rewrites the code. If the performance will drop noticeably, is there other technics that I could use?

Furthermore, I am wondering if there is performance difference between using TList and TObjectList?

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

  TAAtom = array of TAtom;
Nectarine answered 18/3, 2011 at 12:32 Comment(14)
My advice would be to measure it and see for yourself.Maldon
@TOndrej: I am just wondering if it could be estimated for my situation (simple float math upon hundreds upto thousands of instances 30 times per second)... :D There might be a rule of thumb?Nectarine
@Xichen Li, the "Delete" and "Insert" operations are going to be the worst case for all of TList, TObjectList and dynamic array; If you need to think about performance, find a way to skip the penalty of Delete's, the "return on investment" is going to be significantly higher!Crudden
@Xichen Li, do you need random access to any element in the array? If sequential access is enough you'd be better served by the humble "linked list": It provides O(1) insert, delete and append.Crudden
@Cosmin: Thanks very much for your comments! Could you help to suggest some plausible ways to get higher return on investment in my situation?Nectarine
@Xichen Li, my previous comment recommends the use of a linked list, because it has O(1) insert / delete / append. Other ideas include not deleting but replacing the values with nil (so you don't need to shift the buffer) or replacing the whole data structure with an tree: The tree might be able to better balance the cost of random access with the cost of insert and delete. Of course, it all depends on your workload, and only you know what that is.Crudden
@cosmin: I don't make heavy use of random access. But I do sequential access a lot. For example, for each batch of simple float math calculation, I need to iterate through the instances. I assume the linked list data structure is not as good as array in sequential access?Nectarine
@Cosmin: Thank you for helpful suggestions very much! I need to think about them for a while. :DNectarine
No, the linked list works perfectly well for sequential access but can't be used for random access. You need to drop random access all together or accept a really high penalty for using it (you'd need to walk the whole list to get a record by index).Crudden
@Xichen Before you implement linked lists (more complex than lists or arrays), convince yourself that you actually have a performance problem. Otherwise you'll be complicating for no benefit.Wotan
About linked lists: this is a very good pattern, especially for deletion or insertion, but for huge number of items, consider the memory consumption and fragmentation. For sequential reading, a preallocated array of record will be definitively faster than any collection of individual class instances. Each TAtom class instance will have its own memory (calls to GetMem/FreeMem always cost) and won't be contiguous allocated: it will be slower for sequential access. A pre-allocated array of records will be optimized for the CPU L1 and L2 cache.Throw
@A.Bouchez, performance optimization is the art of balancing constraints: Sure, nothing beats an array of records for sequential access, but if you need to do an significant amount of deletes and inserts on a large array, you're going to really feel them. IMHO selecting the best data structures and algorithms are the best possible optimizations, not agonizing over the relative speed of dynamic array and TList.Crudden
@Cosmin Prund That was exactly my point. Amen !Throw
@Cosmin iiuc finding the insertion/deletion point in a linked list is O(n) - #841148Intolerable
W
7

If you use Generics.Collections.TObjectList<TAtom> and there's no need for casting.

Performance should be fine for the usage that you describe. Inserting is more demanding than adding to the end because you need to shift the items after the insertion point up the list.

So long as you avoid SetLength(A, Length(A)+1) and opt for a more sensible allocation strategy dynamic arrays are equivalent to all of the TList like classes.

On occasions I have had problems with performance and memory fragmentation when trying to maintain large lists as contiguous blocks of memory. Then I have resorted to a sub-allocation scheme. But since your lists contain object references which are essentially pointers, you already have implicit sub-allocation.

It's all somewhat speculative and you really need to measure – otherwise we can only guess.

Wotan answered 18/3, 2011 at 12:49 Comment(10)
@David: Thanks very much for your time! But if the old, regular TList/TObjectList is used, could the performance drop be estimated?Nectarine
Old TList/TObjectList performance will be essentially the same as the generic versions.Wotan
@David: Thank you very much for Old TList/TObjectList performance will be essentially the same as the generic versions. Secondly, I understand " Inserting is more demanding", but inserting seems to be necessary to undo the deleting. Furthermore, thank you very much for sharing your experience! I assume you had used array of record? Could you help to comment on the number of instances in your problems?Nectarine
pre-allocation is the trick to use, with external Count variable: think about the dynamic array length as its capacity, not its number of items.Throw
@A.Bouchez Pre-allocation doesn't help when your arrays need very large contiguous blocks of memory, and are constrained by 32 bit address space. Then you need sub-allocation.Wotan
@Xichen Hard to give general advice without knowing your problem. The only area where you have room for manoeuvre is inserting and deleting to/from middle of the list. Do you really need to do this? Anyway, how many insertions do you perform per walk of the list? If that is a small number then you won't have performance problems.Wotan
@David From what Xichen Li wrote above (100 or 1000s of items), data will definitively fit in 32 bit address space. ;)Throw
@David: I only do simple float math on existing instances per walk. The inserting or deleting is done only upon users' requests. For example, one can select a group of atoms and delete them, or one can add a group of atoms into an existing molecule (set of atoms). As you say, it is a small number for most of the time. In this respect, if switching to TList/TObjectList gives almost the same speed for plain sequential access as using dynamic array, TList/TObjectList seems perfectly fine then.Nectarine
@A.Bouchez Since it's only 100 or 1000s of small items containing a handful of floating point values, and one such list in the app, I'd probably be inclined to use Generics.Collections.TList<TMyRecord>. That is a record with methods rather than a class. Contiguous allocation is actually what you want here.Wotan
@Xichen If insertion/deletion is only at user request, then definitely an array/list (same thing really) and not a linked list. And if performance is critical take @A.Bouchez's advice and lay it out in a value type record to get the best cache use.Wotan
T
9

May I add another choice to your list?

If you don't use any inheritance feature for the data in TAtom, you could use a record instead of a class. Each class instance will require to be allocated in memory, filled with zero and initialized individually. Getmem/Freemem always cost, and memory fragmentation will increase.

A pre-allocated dynamic array of record will be faster than individual class instances for adding. And the data will fit better for CPU L1/L2 cache.

For inserting and deleting, an array of such records will be slower than TList if you have a huge number of items, because there'll be more data to delete/insert (TList/TObjectList both maintain just a list of pointers). For even faster insertion/deletion, you should better use a linked list.

There is some overhead in the TList/TObjectList mechanism because of internal notification. mechanism And the GetItem() property could be a bit slower (because of range checking) than using directly a dynamic array.

But with our TDynArray wrapper, you could stick to a dynamic array, and still have good performance, pre-allocation features, and TList-like methods. And even more methods available, like SaveToStream, Slice, Reverse, sorting with external indexes and such...

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

Works with Delphi 6 up to XE.

With newer version of Delphi supporting generics, you should better go into this direction.

Throw answered 18/3, 2011 at 13:1 Comment(2)
Thank you very much for your suggestion! I will try first to understand your code before further commenting. :DNectarine
Feel free to ask question here or in our forum, if you need information.Throw
W
7

If you use Generics.Collections.TObjectList<TAtom> and there's no need for casting.

Performance should be fine for the usage that you describe. Inserting is more demanding than adding to the end because you need to shift the items after the insertion point up the list.

So long as you avoid SetLength(A, Length(A)+1) and opt for a more sensible allocation strategy dynamic arrays are equivalent to all of the TList like classes.

On occasions I have had problems with performance and memory fragmentation when trying to maintain large lists as contiguous blocks of memory. Then I have resorted to a sub-allocation scheme. But since your lists contain object references which are essentially pointers, you already have implicit sub-allocation.

It's all somewhat speculative and you really need to measure – otherwise we can only guess.

Wotan answered 18/3, 2011 at 12:49 Comment(10)
@David: Thanks very much for your time! But if the old, regular TList/TObjectList is used, could the performance drop be estimated?Nectarine
Old TList/TObjectList performance will be essentially the same as the generic versions.Wotan
@David: Thank you very much for Old TList/TObjectList performance will be essentially the same as the generic versions. Secondly, I understand " Inserting is more demanding", but inserting seems to be necessary to undo the deleting. Furthermore, thank you very much for sharing your experience! I assume you had used array of record? Could you help to comment on the number of instances in your problems?Nectarine
pre-allocation is the trick to use, with external Count variable: think about the dynamic array length as its capacity, not its number of items.Throw
@A.Bouchez Pre-allocation doesn't help when your arrays need very large contiguous blocks of memory, and are constrained by 32 bit address space. Then you need sub-allocation.Wotan
@Xichen Hard to give general advice without knowing your problem. The only area where you have room for manoeuvre is inserting and deleting to/from middle of the list. Do you really need to do this? Anyway, how many insertions do you perform per walk of the list? If that is a small number then you won't have performance problems.Wotan
@David From what Xichen Li wrote above (100 or 1000s of items), data will definitively fit in 32 bit address space. ;)Throw
@David: I only do simple float math on existing instances per walk. The inserting or deleting is done only upon users' requests. For example, one can select a group of atoms and delete them, or one can add a group of atoms into an existing molecule (set of atoms). As you say, it is a small number for most of the time. In this respect, if switching to TList/TObjectList gives almost the same speed for plain sequential access as using dynamic array, TList/TObjectList seems perfectly fine then.Nectarine
@A.Bouchez Since it's only 100 or 1000s of small items containing a handful of floating point values, and one such list in the app, I'd probably be inclined to use Generics.Collections.TList<TMyRecord>. That is a record with methods rather than a class. Contiguous allocation is actually what you want here.Wotan
@Xichen If insertion/deletion is only at user request, then definitely an array/list (same thing really) and not a linked list. And if performance is critical take @A.Bouchez's advice and lay it out in a value type record to get the best cache use.Wotan
M
4

However, because type-casting is needed using the regular TList/TObjectList, could some one comment on the possible performance hit?

If you type cast using the form

List[I] as TAtom

as small overhead will be added, which can really add up in your situation. However, if you hard typecast

TAtom(List[I])

as far as I know, no overhead actually occurs as the typecast is performed without checks and I also believe it is done at compile time.

As for the other aspect of your question, I think they have been all properly covered already...

Mummer answered 18/3, 2011 at 14:22 Comment(5)
You are right: typecast is done at compile time. If you want an even faster execution (without any Get method call), you could write TAtom(List.List[I]). With this, you'll directly map the internal pointer list, so code will be as fast as possible (just a few asm opcodes). But you should be sure that I variable is correct (like for I := 0 to List.Count-1 do), because in this case there will be no range check.Throw
Thank you very much for your comment! Added another link of hard-cast versus as-cast just in case others find it useful. delphi.about.com/od/delphitips2007/qt/is_as_cast.htmNectarine
@A.Bouchez: Great to know that! Taking the TForceList in this stackflow page for example: If I have var tmpList: TForceList;, I guess I should use TForce(tmpList.Items[I] instead of TForce(tmpList.Objects[I]), to get what you meant? Could you help to comment?Nectarine
TForceList.GetObject will call TForceList.Get so TForce(tmpList.Objects[I]) is slower than TForce(tmpList.Items[I]). The faster will be TForce(tmpList.List[I]) (no method call at all, just fast pointer arithmetic in asm).Throw
@Xichen Li about typecast as stated in your quoted SO page: take care you should typecast a pointer or TObject to NativeInt instead of Integer to be ready for the 64 bit Delphi compiler (which should be released soon). Let have our code prepared for it!Throw
R
2

Make a test project and measure the time of adding, inserting and deleting thousands of TAtom instances using the four methods. Then decide which one to use.

TList and TObjectList are probably faster than a dynamic array when adding, inserting and deleting, because the dynamic array constantly has to be reallocated. The implementation of TList and TObjectList doesn't do that.

Radiance answered 18/3, 2011 at 12:42 Comment(5)
Thanks for your time! I am just wondering if it could be estimated for my situation (simple float math upon hundreds upto thousands of instances 30 times per second).Nectarine
A dynamic array of TAtom class instances will have the same timing as TList of TObjectList because all use slow move() when inserting and deleting. For adding, a dynamic array is as fast as a TList if you pre-allocate the memory, i.e. use SetLength() to set the capacity, then use an external integer variable for the Count. TList and TObjectList may even be a bit slower than dynamic array because of the internal notification mechanism.Throw
@A.Bouchez: I am talking about a plain dynamic array, not a class that uses a dynamic array internally. And yes, maybe deleting is slower, but I think TList/TObjectList is faster with random deletes. But then again, you should just measure it.Radiance
TList/TObjectList will move() all the pointers for random deletion (and also at insertion). So it will be the same speed as manual dynamic array deletion. If you were talking about our TDynArray, please take note that it is not "a class that uses a dynamic array internally", but a wrapper to access a dynamic array. The data is not stored in TDynArray, but in the dynamic array. TDynArray just provide generic methods to access the array data, thanks to the fast RTTI information available since the first version of Delphi.Throw
@A.Bouchez: I read your answer and read pre-allocation features. So I thought you were talking about a class where Capacity <> Count. And for that you have to store data. But with Capacity you just mean the Length of the dynamic array.Radiance
E
1

As TObjectList is a direct descendant of TList the performances will be very close.

Emmettemmey answered 18/3, 2011 at 12:42 Comment(0)
C
1

First question: Are we talking about Classes.TList and Contnrs.TObjectList or are we talking about Generics.Collections.TList respectively Generics.Collections.TObjectList ?

If we're talking about generics, both TList and TObjectList are implemented using dynamic arrays. If there's any performance difference between directly using a dynamic array or using the nicer interface of the generic container, it's going to be negligible.


If we're talking about the "older" TList and TObjectList, then we need to compare only TList with the dynamic array equivalent, since TObjectList is an descendant of TList so it inherits all it's performance characteristics. TList uses a block of memory allocated using ReallocMem. The dynamic array does the same thing internally, so there shouldn't be a significant difference!

Conclusion

If there's any performance difference between the two, it's probably because naive use of dynamic array uses the dreaded SetLength(A, Length(A)+1), while the better implementation in all Delphi-provided containers pre-allocate memory in larger chunks. With proper code there shouldn't be any significant difference between those alternative!

Crudden answered 18/3, 2011 at 12:48 Comment(1)
Thank you very much for your helpful comments! I am considering old, regular TList/TObjectList for now. Great to know ..., so there shouldn't be a significant difference!, and ..., while the better implementation in all Delphi-provided containers pre-allocate memory in larger chunks!Nectarine
D
1

TList etc. do exactly what code working on chunks of memory or dynarrays would have to do, but their implementation is already optimized for the common case, and includes considerations about how the memory manager behaves.

One criteria could be the ratio of reads/updates to the sequence. If the sequence is updated infrequently after created, then there should be perceivable better speed with dynarrays, because access to elements with TList and the likes requires one method call plus bounds checking, plus a type check if you use the as operator.

In the end, the cost of arithmetic done on TAtom should dominate the run time, making the choice of dynarray or TListXXX irrelevant.

Davidadavidde answered 18/3, 2011 at 13:2 Comment(3)
@Thanks for your comments! Indeed I do this a lot: "If the sequence is updated infrequently after created...".Nectarine
Sorry for the typo! I meant the sequence is updated frequently after its creation.Nectarine
@Xichen if you have frequent updates, then by all means use the optimized implementations in the standard library.Davidadavidde
S
1

A dynamic arrays of records will have the lowest impact when accessing the items, if your atoms are objects, then all the lists are going to be somewhat equivalent in terms of access speed.

However, if you perform many of them, your key issue will be the inserts and deletes, for which all lists and arrays will perform poorly, but that is what profiling will tell you. If that's the case, you might then want to consider:

  • a linked list if you don't need to access atoms by index
  • a tree, should you have use for a space partition of your atoms, you might as well use that partition to hold your atoms rather than the array
  • allowing undefined/nil elements in your array/list, and maintaining a stack of undefined/nil elements, and an index if you need a sorted list (potentially the highest performance solution, but also likely the most complex to get right in terms of efficiency)
Sensualist answered 18/3, 2011 at 13:29 Comment(7)
Just curious, did you read the comments on the question or the already provided answers? Because you're apparently summarizing that information. Really looks like plagiarismCrudden
@Cosmin Prund After his works, I doubt Eric Grange don't know about data structures, and just copied his answer from comments. ;)Throw
Eric -- Good to see you answering questions here on SO.Tana
@A.Bouchez, well then, it's a good thing I gave him the benefit of doubt and didn't downvote!Crudden
@Eric: Thanks very much for your kind help! I will think about your suggestion of the highest performance solution.Nectarine
@A.Bouchez, @Cosmin: Yes he is the talented guy reviving DWScript. :)Nectarine
@Cosmin: the two first sentences are just introductions, and yes, they don't have much detailed over what was already posted. But the point I was making is that the performance of the array-based lists may be irrelevant, if inserts/deletes are significant, non-array-based "lists" like the three I mentioned would be the ones to investigate. @others: thanks ;)Sensualist

© 2022 - 2024 — McMap. All rights reserved.