问题 多个单元中的泛型实例化是否会破坏可执行文件?


这个 Embarcadero文章 讨论XE7 IDE的内存问题包含以下内容:

注意“泛型增长”

另一种可能取决于您的应用程序代码并导致编译器和调试器使用的内存增加的情况与使用通用数据类型的方式有关。 Object Pascal编译器的工作方式可以导致基于相同的通用定义生成许多不同类型,有时甚至是在不同模块中编译的完全相同的类型。虽然我们不一定会建议删除泛型,但恰恰相反,有几个选项需要考虑:

  • 尽量避免定义核心泛型类型的单元的循环单元引用
  • 尽可能定义和使用相同的具体类型定义
  • 如果可能,重构泛型以在基类中共享代码,泛型类从中继承

我理解的最后一项。前两个我不太清楚。

这些问题是否只影响IDE性能,还是会影响编译代码的大小?

例如,考虑第二项,如果我申报 TList<Integer> 在两个单独的单元中,我将在可执行文件的每个单元中获得两个单独的代码块吗?我当然希望不是!


12930
2017-07-28 18:24


起源

如何链接没有所用泛型类型代码的DCU?编译器无法知道该单元将始终与另一个共享泛型类型的单元一起编译。它需要是可以从DCU追溯删除重复泛型类型的链接器。这似乎很有挑战...... - J...
@J ......那是Embarcadero的问题。 C ++和C#工具链是否有任何问题? - David Heffernan
我并不反对,我只是观察他们让它滑落的可能原因。 - J...
对于您(或任何人)的知识,是否有更新版本的Delphi(西雅图/柏林?)中有关此主题的更改。 - Tupel


答案:


第2点。 这指的是在可能的情况下实例化相同的泛型类型。例如使用 TList<Integer> 在所有地方,而不是有两个泛型类型 TList<Integer> 和 TList<SmallInt>

声明和使用 TList<Integer> 在几个单位将只包括 单一副本 的 TList<Integer> 在exe文件中。另外,宣布 TIntegerList = TList<Integer> 会产生同样的结果。

普通臃肿的人指的是有完整的 TList<T> 复制您使用的每种特定类型,即使基础生成的代码是相同的。

例如: TList<TObject> 和 TList<TPersistent> 将包括两个单独的副本 TList<T> 即使生成的代码可以折叠成单个代码。

这让我们感动 第3点。 在公共类代码中使用基类,然后在其上使用泛型类来获得类型安全性,可以在编译期间和exe文件中节省内存。

例如,在非泛型上构建泛型类 TObjectList 只会为每种特定类型包含精简通用图层而不是完整图层 TObjectList 功能。报道为 QC 108966

  TXObjectList<T: class, constructor> = class(TObjectList)
  protected
    function GetItem(index: Integer): T;
    procedure SetItem(index: Integer; const Value: T);
  public
    function Add: T;
    property Items[index: Integer]: T read GetItem write SetItem; default;
  end;

function TXObjectList<T>.GetItem(index: Integer): T;
begin
  Result := T( inherited GetItem(index));
end;

procedure TXObjectList<T>.SetItem(index: Integer; const Value: T);
begin
  inherited SetItem(index, Value);
end;

function TXObjectList<T>.Add: T;
begin
  Result := T.Create;
  inherited Add(Result);
end;

7
2017-07-28 19:52



从TObjectList派生会阻止您编写接受的代码 TList<T> 和供应 TObjectList<T>。 Emba的XE8改变了System.Generics以不同的方式攻击了这个,但他们搞砸了,因为他们没有像样的单元测试。你知道,我确信。最终结果是无法读取的可怕RTL代码。他们应该咬紧牙关并正确地完成工作。在我看来,一半的解决方案比以前更糟糕。感谢您对第2点的评论。 - David Heffernan
Dalija,这不是真的。我刚编译了一个简单的控制台应用程序,它有两个单元,Unit4和Unit5。 Unit4有一个程序Test4,而Unit5有一个程序Test5。如果我使Test5为空,但Test4使用TList <Integer>,则.exe大小为2048k。如果我给Test5完全相同的代码,.exe是2067k。所以这增加了19k到可执行文件。 - Rudy Velthuis
@RudyVelthuis我将不得不重新检查控制台应用程序。通过常规应用我的观点。自Delphi XE以来,我一直在与泛型膨胀作斗争,所以我非常清楚我在谈论什么。 - Dalija Prasnikar
“常规”应用程序已经在表面下使用了很多泛型,因此更多的实例化不会产生很大的不同。但是大小仍然存在差异,每个TList <T>都会重新实例化。 - Rudy Velthuis
@RudyVelthuis我在不同单位使用TList <Integer>时的大小增加为零,即使它们在不同的包中声明。不可能错过19K的尺寸增加。 - Dalija Prasnikar


他们在文章中讨论的代码膨胀(因为它是关于IDE中的内存不足问题)与生成的DCU和IDE中保存的所有元信息有关。每个DCU都包含所有使用的泛型。只有在编译二进制文件时,链接器才会删除重复项。

这意味着如果你有 Unit1.pas 和 Unit2.pas 两者都在使用 TList<Integer> 都 Unit1.dcu 和 Unit2.dcu 有二进制代码 TList<Integer> 汇编。

如果你宣布 TIntegerList = TList<Integer> 在 Unit3 并使用它 Unit1 和 Unit2你可能会认为这只包括已编译的 TList<Integer> 在 Unit3.dcu 但不是在另外两个。但不幸的是情况并非如此。


5
2017-07-29 19:51



所以在这种情况下有 TIntegerList = TList<Integer> 在某个公共单元中声明可以帮助保留IDE中的内存,但不会对二进制文件产生任何影响。 - Dalija Prasnikar
@DalijaPrasnikar不幸的是,因为那只是一个别名。它仍然会将类型编译到每个单元中(刚刚测试过)。只写 TIntegerList = class(TList<Integer>) 会解决这个问题 - 而且这并不是真正的解决方案。 - Stefan Glienke
谢谢......我们所做的一切都不是解决方案,但是在某些情况下,了解变通办法可以提供帮助。你能否在你的答案中加入你的解释。 - Dalija Prasnikar
令我感到遗憾的是,他们让RTL人员在XE8中销毁Sytem.Generics.Collections中的类而不是让编译人员解决真正的问题,这是一种可怕的耻辱。同样,Spring系列也以类似的方式进行了重新设计。所需要的只是链接器检查每个实例化方法,寻找要合并的重复项。 - David Heffernan