【问题标题】:How noticeable is the difference of performance among TList, TObjectList, and plain array, if it could be estimated?如果可以估计,TList、TObjectList 和普通数组之间的性能差异有多明显?
【发布时间】:2011-07-18 03:28:35
【问题描述】:

*总结:

请查看 Delphi 专家的知识渊博的 cmets。特别是对我来说,我会尝试按照 David 的建议使用旧的 TList/TObjectList,并按照 A.Bouchez 的建议使用 hard-cast 和 TObjectList.List 属性。以后重构的时候会试试TDynArray。

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

假设我有一个TAtom 类,如以下代码中所定义。在运行时大约有 hundredsthousands 的 TAtom 实例,目前为 stored in a dynamic array。在运行时,我需要对所有现有 TAtom 实例的 TAtom.X/Y/Z 进行简单的浮点数学运算,超过每秒 30 次。

现在,我需要在运行时添加 TAtom 实例的addinginsertingdeleting 的能力。看来我的选择是(1)请求一个大数组; (2)坚持动态数组,手动设置长度; (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;

【问题讨论】:

  • 我的建议是测量它并亲自查看。
  • @Xichen Li,“删除”和“插入”操作将是所有 TList、TObjectList 和动态数组的最坏情况;如果你需要考虑性能,想办法跳过删除的惩罚,“投资回报率”会明显更高!
  • @Xichen Li,你需要随机访问数组中的任何元素吗?如果顺序访问就足够了,那么简陋的“链表”会更好地为您服务:它提供 O(1) 的插入、删除和追加。
  • @Xichen Li,我之前的评论推荐使用链表,因为它有 O(1) 的插入/删除/追加。其他想法包括不删除但用 nil 替换值(因此您不需要移动缓冲区)或用树替换整个数据结构:树可能能够更好地平衡随机访问的成本与成本插入和删除。当然,这一切都取决于你的工作量,只有你自己知道。
  • 不,链表非常适合顺序访问,但不能用于随机访问。您需要完全放弃随机访问或接受使用它的非常高的惩罚(您需要遍历整个列表才能按索引获取记录)。

标签: arrays delphi tlist tobjectlist


【解决方案1】:

但是,因为类型转换是 需要使用常规 TList/TObjectList,可以有一个 评论可能的表现 打?

如果您使用表单键入 cast

List[I] as TAtom

因为会增加少量开销,这在您的情况下确实会增加。但是,如果您进行硬类型转换

TAtom(List[I])

据我所知,实际上没有开销,因为类型转换是在没有检查的情况下执行的,我也相信它是在编译时完成的。

至于你问题的另一方面,我认为它们都已经被适当地涵盖了......

【讨论】:

  • 你是对的:类型转换是在编译时完成的。如果你想要更快的执行(没有任何Get 方法调用),你可以写TAtom(List.List[I])。这样,您将直接映射内部指针列表,因此代码将尽可能快(只需几个 asm 操作码)。但是你应该确保I 变量是正确的(比如for I := 0 to List.Count-1 do),因为在这种情况下不会有范围检查。
  • 非常感谢您的评论!添加了另一个硬铸造与原铸的链接,以防其他人发现它有用。 delphi.about.com/od/delphitips2007/qt/is_as_cast.htm
  • @A.Bouchez:很高兴知道这一点!以this stackflow page 中的TForceList 为例:如果我有var tmpList: TForceList;,我想我应该使用TForce(tmpList.Items[I] 而不是TForce(tmpList.Objects[I]),来理解你的意思?你能帮忙评论一下吗?
  • TForceList.GetObject 将调用TForceList.Get 所以TForce(tmpList.Objects[I])TForce(tmpList.Items[I]) 慢。更快的是TForce(tmpList.List[I])(根本没有方法调用,只是asm中的快速指针运算)。
  • @Xichen Li 关于类型转换,如您引用的 SO 页面中所述:注意您应该将指针或 TObject 类型转换为 NativeInt 而不是 Integer,以便为 64 位 Delphi 编译器(应该很快发布)做好准备)。让我们为它准备好代码!
【解决方案2】:

我可以在您的列表中添加另一个选择吗?

如果您不对TAtom 中的数据使用任何继承功能,则可以使用record 而不是class。每个类实例都需要在内存中分配,用零填充并单独初始化。 Getmem/Freemem 总是消耗,而且内存碎片会增加。

预先分配的动态array of record 将比添加单个类实例更快。并且数据将更适合 CPU L1/L2 缓存。

对于插入和删除,如果您有大量项目,此类记录的数组将比 TList 慢,因为要删除/插入的数据更多(TList/TObjectList 都只维护一个列表指针)。为了更快地插入/删除,您最好使用链表。

由于内部通知,TList/TObjectList 机制存在一些开销。机制并且GetItem() 属性可能比直接使用动态数组要慢一些(因为范围检查)。

但是使用我们的TDynArray wrapper,您可以坚持使用动态数组,并且仍然具有良好的性能、预分配功能和类似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 支持泛型,您最好朝这个方向发展。

【讨论】:

  • 非常感谢您的建议!在进一步评论之前,我将首先尝试理解您的代码。 :D
  • 如果您需要信息,请随时在此处或我们的论坛中提问。
【解决方案3】:

在访问项目时,动态记录数组的影响最小,如果您的原子是对象,那么所有列表在访问速度方面都将是相当的。

但是,如果您执行其中的许多操作,您的关键问题将是插入和删除,所有列表和数组的性能都会很差,但这就是分析会告诉您的。 如果是这种情况,您可能需要考虑:

  • 如果您不需要按索引访问原子,则为链表
  • 一棵树,如果您有用于原子的空间分区,您不妨使用该分区来保存您的原子而不是数组
  • 在数组/列表中允许未定义/无元素,并维护一堆未定义/无元素,如果需要排序列表,还可以使用索引(可能是性能最高的解决方案,但也可能是最复杂的解决方案效率方面)

【讨论】:

  • 只是好奇,您是否阅读了有关问题的 cmets 或已经提供的答案?因为你显然是在总结这些信息。真的很像plagiarism
  • @Cosmin Prund 在他的作品之后,我怀疑 Eric Grange 不了解数据结构,只是从 cmets 复制了他的答案。 ;)
  • Eric -- 很高兴看到你在这里回答问题。
  • @A.Bouchez,那么,我给了他怀疑的好处并且没有投反对票,这是一件好事!
  • @Eric:非常感谢您的帮助!我会考虑你对the highest performance solution的建议。
【解决方案4】:

TList 等完全执行在内存块或 dynarray 上工作的代码必须执行的操作,但它们的实现已经针对常见情况进行了优化,并且包括对内存管理器行为方式的考虑。

一个标准可以是读取/更新与序列的比率。如果序列在创建后不经常更新,那么使用 dynarrays 应该会有更好的速度,因为使用 TList 访问元素等需要一个方法调用和边界检查,如果使用 as 运算符,还需要进行类型检查.

最后,在 TAtom 上完成的算术成本应该在运行时间中占主导地位,因此选择 dynarray 或 TListXXX 无关紧要。

【讨论】:

  • @感谢您的 cmets!事实上,我经常这样做:“If the sequence is updated infrequently after created...”。
  • 抱歉错字!我的意思是序列在创建后更新frequently
  • @Xichen 如果你经常更新,那么一定要使用标准库中优化的实现。
【解决方案5】:

如果你使用Generics.Collections.TObjectList<TAtom> 并且不需要强制转换。

性能应该适合您描述的用途。插入比添加到末尾要求更高,因为您需要将插入点之后的项目向上移动。

只要避免SetLength(A, Length(A)+1) 并选择更明智的分配策略,动态数组就等同于所有TList 类。

在尝试将大型列表维护为连续的内存块时,有时我会遇到性能和内存碎片问题。然后我采用了子分配方案。但是由于您的列表包含本质上是指针的对象引用,因此您已经有了隐式子分配。

这都是推测性的,你真的需要衡量——否则我们只能猜测。

【讨论】:

  • 旧 TList/TObjectList 性能将与通用版本基本相同。
  • 预分配是使用外部计数变量的技巧:将动态数组长度视为它的容量,而不是它的项目数。
  • @A.Bouchez 当您的阵列需要非常大的连续内存块并且受到 32 位地址空间的限制时,预分配无济于事。然后你需要子分配。
  • @Xichen 在不知道您的问题的情况下很难给出一般性建议。您唯一有回旋余地的区域是在列表中间插入和删除。你真的需要这样做吗?无论如何,您每次遍历列表执行多少次插入?如果这是一个小数字,那么您不会遇到性能问题。
  • @David 从 Xichen Li 上面写的(100 或 1000 个项目)中,数据将完全适合 32 位地址空间。 ;)
【解决方案6】:

做一个测试项目,用四种方法测量添加、插入和删除数千个TAtom实例的时间。然后决定使用哪一个。

TList 和 TObjectList 在添加、插入和删除时可能比动态数组快,因为动态数组必须不断地重新分配。 TList 和 TObjectList 的实现没有这样做。

【讨论】:

  • 感谢您的宝贵时间!我只是想知道是否可以根据我的情况进行估计(每秒 30 次对数百到数千个实例进行简单的浮点数学运算)。
  • TAtom 类实例的动态数组将与 TObjectList 的 TList 具有相同的时序,因为在插入和删除时都使用慢速 move()。对于添加,如果您预先分配内存,则动态数组与 TList 一样快,即使用 SetLength() 设置容量,然后为 Count 使用外部整数变量。由于内部通知机制,TList 和 TObjectList 甚至可能比动态数组慢一点。
  • @A.Bouchez:我说的是一个普通的动态数组,而不是一个在内部使用动态数组的类。是的,也许删除速度较慢,但​​我认为 TList/TObjectList 随机删除速度更快。但话又说回来,你应该只是测量它。
  • TList/TObjectList 将 move() 随机删除(以及插入时)的所有指针。所以它会和手动动态数组删除一样的速度。如果您在谈论我们的 TDynArray,请注意它不是“内部使用动态数组的类”,而是访问动态数组的 包装器。数据不是存储在 TDynArray 中,而是存储在动态数组中。 TDynArray 仅提供访问数组数据的通用方法,这要归功于 Delphi 第一个版本以来提供的快速 RTTI 信息。
  • @A.Bouchez:我阅读了您的回答并阅读了pre-allocation features。所以我以为您在谈论容量计数的课程。为此,您必须存储数据。但是对于容量,您只是指动态数组的长度。
【解决方案7】:

第一个问题:我们是在谈论Classes.TListContnrs.TObjectList,还是分别在谈论Generics.Collections.TList Generics.Collections.TObjectList

如果我们谈论泛型,TList 和 TObjectList 都是使用动态数组实现的。如果直接使用动态数组或使用通用容器的更好接口之间有任何性能差异,那将可以忽略不计。


如果我们谈论的是“旧”TListTObjectList,那么我们只需要将 TList 与等效的动态数组进行比较,因为 TObjectListTList 的后代,所以它继承所有它的性能特征。 TList 使用使用ReallocMem 分配的内存块。动态数组在内部做同样的事情,所以应该没有显着差异!

结论

如果两者之间有任何性能差异,可能是因为天真地使用动态数组使用了可怕的SetLength(A, Length(A)+1),而在所有 Delphi 提供的容器中更好的实现预先分配了更大的内存块。使用正确的代码,这些替代方案之间应该没有任何显着差异!

【讨论】:

  • 非常感谢您提供帮助的 cmets!我现在正在考虑旧的常规 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
【解决方案8】:

由于 TObjectList 是 TList 的直接后代,因此性能将非常接近。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-07-30
    • 1970-01-01
    相关资源
    最近更新 更多