【问题标题】:Optimizing Class Size in Delphi. Is there something like "packed classes"?在 Delphi 中优化类大小。有没有类似“打包类”的东西?
【发布时间】:2009-05-06 18:46:42
【问题描述】:

我正在尝试优化我的 Delphi 类的大小,以便它们占用尽可能少的内存,因为我正在创建大量它们。

问题是,课程本身非常小,但并没有占用我所期望的空间。例如,如果我有

type MyClass = class
  private
    mMember1 : integer;
    mMember2 : boolean;
    mMember3 : byte;
end;

我希望它使用 6 个字节,但是,由于对齐,它最终使用 12 个字节,这是布尔值使用 4 个字节而不是 1 个字节......字节字段也是如此......

对于记录,您可以使用 {$A1} 指令或将其声明为打包记录以使其仅使用所需的内存。

有没有办法用类做同样的事情? (也许一些关于如何正确覆盖 NewInstance 类方法的教程?)

编辑:好的,关于我在做什么的一点解释......

首先,真正的类大小大约是 40 字节,包括 VMT 和接口指针占用的空间。

这些类都继承自一个大小为 8 字节的基本 RefCounting 类(一个整数 FRefCount 和一些允许引用计数的方法),并且它们必须支持接口(因此根本不使用打包记录)。

这些对象被传递并被投射到几个东西上,而处理程序不知道他们得到了什么。例如,我有一个接收 TItem 列表并执行以下操作的类:

if Supports(List[i], IValuable, IValInstance) then
  Eval(IValInstance.Value);

然后另一个处理程序可能会检查其他接口

If Supports(List[i], IStringObject, IStringInstance) then
  Compose(IStringInstance.Value)

这样每个 Handler 都会对 List 进行不同的处理...

关于如何获得类的总大小,我使用了一个修改后的内存管理器,以便我可以跟踪“真正的”内存管理器为该类使用了多少内存。通过这种方式,我非常有信心实例不会被打包。

最后这是在 Delphi 7 中。我尝试使用 {$A1} 预编译器指令,但没有成功,字段以任何方式对齐,我可能有数百万个实例作为最坏的情况,因此节省 6 个字节可以保存几 MB 的结果。

【问题讨论】:

  • 如果你把关键字“packed”放在class前面会发生什么? (看起来你已经忽略了那里的代码)
  • 编译器错误:P 你可以使用打包的记录
  • 您如何精确测量?看到我对该类的计算是字段的 6 个字节,并且打包确实对我有用,可能取决于编译器版本以及您是否正在为 .NET 编译?我的是 Delphi 2007,没有 .NET
  • 我已经扩展了为什么我正在做我正在做的事情以及为什么打包记录不是一个选项。对不起,我第一次弄糊涂了。其中的数字只是一个例子来说明这一点,当然你必须考虑到虚拟方法表的空间以及类中存在的任何接口指针。
  • 豪尔赫,你误会了。如果您将所有字段放入打包记录中,这并不妨碍您使用接口。编译器在定义类的布局时会自动为您处理接口字段。记录字段不会对此产生干扰。此外,接口字段不会影响对齐,因为它们无论如何都是四字节值。您也不需要特殊的内存管理器来衡量这一点;只需在类上使用 InstanceSize 函数即可。

标签: delphi optimization memory


【解决方案1】:

您可以使用打包记录作为对象的字段:

type
  TMyRecord = packed record
    Member1 : integer;
    Member2 : boolean;
    Member3 : byte;
  end;

  TMyClass = class
  private
    FData : TMyRecord;
   function GetMember1 : Integer;
  public
    property Member1 : Integer read GetMember1;
    // Later versions of Delphi allow "read FData.Member1;", not sure when from
  end;

function TMyClass.GetMember1 : integer;
begin
  result := FData.Member1;
end;

【讨论】:

  • +1,但如果 OP 不需要所有单个数据元素的完整范围,他应该使用stackoverflow.com/questions/282019 中概述的技术并将布尔值折叠成其中之一的一位其他数据元素,在此过程中节省另一个字节。在现代处理器上,以更多 cpucycles 为代价来减少内存消耗几乎每次都能赢得性能。
  • 还请注意,您可以将整个打包记录放入 TMyClass,创建一个私有的未命名类型(因为它是一个实现细节)。
  • "Delphi 的后期版本允许"读取 FData.Member1;",不确定何时从"D7 已经可以做到这一点
【解决方案2】:

如果您非常担心几个字节(您提到了 6 对 12),那么您根本不应该使用类。请改用记录。然后可以使用 packed 来消除对齐浪费;但是,请准备好承受性能损失,因为默认的“非压缩”对齐设置是为了让 CPU 以最快的速度访问。

【讨论】:

  • 如果他的“很大数量”是几百万,那么节省50%是一件大事。
  • 如果您需要一个类的“几百万”个实例,那么首先考虑不将这么多实例加载到内存中来节省可能也是值得的。
  • 我同意 Rob 的观点。如果你需要几百万个东西,可能有更好的方法来完成你想做的事情。 @Mason:50%,当它是 6 个 字节 时,没什么大不了的。即使你说了很多。它是一个整数和一半的空间 - 删除几个未使用的变量,或者在可以的情况下使用 Byte 代替几个地方的整数,这样可以节省很多。我相信你听说过“过早优化”这个词......
【解决方案3】:

为什么不一开始就使用打包记录呢?它会忽略从 TObject 下降引起的开销(轻微)......

【讨论】:

    【解决方案4】:

    当然。您可以打包集合、数组、记录、对象和文​​件类型。请注意,使用打包确实会导致访问数据时速度变慢,并且可能会导致一些类型兼容性问题。

    我在 Delphi 2006 中对此进行了尝试。编辑器的语法检查将其标记为错误,但编译得很好。

    根据 Delphi 文档,$A 开关适用于类类型和记录类型。

    更新:

    我也在 Delphi 6 中尝试过。它编译成功。如果打包类在 Delphi 7 中无法编译,您可能已经发现了一个错误。如果这是一个错误,Embarcadero 不太可能对此采取任何措施,除非它仍然出现在最新版本的 Delphi 中,但似乎并非如此。

    【讨论】:

    • 我会在 Delphi 7 中再试一次,但是,已经尝试过一次,我很有信心 Delphi 7 只是忽略了类的预编译器指令......这很令人沮丧......
    • 你可以有打包的对象类型,但不能有打包的类。编译器允许语法,但它没有任何效果。我们谈论的是类,而不是旧式对象。
    【解决方案5】:

    也许有点跑题了,但我之前(在 D2006 之前,所以没有记录)为某些 ORM 框架而苦苦挣扎。假设“类”的东西是一成不变的:

    提示和提示:

    1. 我通过为字段设置 getter 和 setter 来解决打包问题,将它们存储在类的字节数组中。甚至可以被位压缩。如果 setter/getter 是可内联的(那么对我来说不是一个选项,D6),它甚至可能相当便宜。
    2. 尝试通过自己初始化一块内存、设置 VMT 并调用其上的构造函数来收集堆分配(管理开销和松弛空间)开销。 IIRC 堆开销为 8 字节,旧 heapmgr 的分配粒度为 8 字节,fastmm 为 16 字节。如果根据大小对类进行排序,则可以使用位图作为分配结构
    3. 如果您特别邪恶,请记住指针有 2 或 3 位松弛。我将这些位用作非常常用的分配类型的标识,节省了堆保留的 4 个字节来存储大小。
    4. 注意您的索引。如果你有很多对象(我有大约 600 万个),你也必须小心你的索引类型。 (请不要使用 tstringlist)
    5. 始终将未混淆的内容保留在 ifdef 下,以便于调试测试 (*)
    6. 永远不要使用字符串作为键。必要时散列。规范化结构不仅对数据库有好处

    (*) 我后来在 64 位 FPC 下重新编译了“干净”版本,尽管第 1 点和第 2 点很丑陋,但它在一些小的 sizeof(pointer()) 之后仍然有效

    【讨论】:

    • 覆盖 NewInstance 和 FreeInstance 可能有助于执行第 2 点。
    【解决方案6】:

    手动打包数据。取每 4 个字节并将它们放在一个基数中。如果您有两个长度不是 4 的倍数的短刺,则将它们放入一个短字符串中,然后读出每个部分的部分。

    这将需要您进行更多的工程设计,手动排列所有内容,但通过使用 getter 和 setter,行为将在类外透明。您可以非常接近与编译器以这种方式打包它的结果。

    【讨论】:

      【解决方案7】:

      如果您将拥有大量实例并希望避免与单独分配相关的开销,您可以使用打包记录并在类本身外部维护它们,例如通过一个或多个大型分配数组。

      然后,在类中,您只能存储一两个字段用于索引到堆和偏移量中。如果您可以只使用一个大内存块,则可以将其减少到仅偏移量。

      TPackedRecord = packed record ... end;
      PPackedRecord = ^TPackedRecord;
      TPackedRecordHeap = class
        ...
        function  Add: PPackedRecord;
        procedure Release( entry: PPackedRecord );
      end;
      
      TUsableClass = class
      private
        heap: TPackedRecordHeap;
        data: PPackedRecord;
      public
        constructor Create( heap: TPackedRecordHeap );
        ...
      end;
      

      【讨论】:

        【解决方案8】:

        仅供参考,如果那是一条记录,它将是 8 个字节,而 6 个字节作为打包记录。因此,您正在查看类指针的 4 字节开销(假设您在 2009 年之前使用 Delphi),如果它被打包,则有可能回收 2 个字节。

        【讨论】:

          【解决方案9】:

          我希望它使用 6 个字节,但是由于对齐,它最终使用了 12 个字节

          即使你写“TMyClass = class end;”该类将从具有虚方法的 TObject 继承。

          这让

            4 Bytes (VMT)
          + 4 Bytes (member1: Integer)
          + 1 Byte  (member2: Boolean)
          + 1 Byte  (member3: Byte);
          + 2 Bytes (alignment)
          ---------
           12 Bytes
          

          因此,如果您禁用对齐,您将只赢得 2 个字节。

          通过按数据类型大小(在您提到的较大类中)对字段进行排序,可以消除一些对齐漏洞。并且 $A- (Delphi 5) 或 $A1 (更新) 不起作用。在 Delphi 7 和 Delphi 2009 中都没有。

          顺便说一句:在 Delphi 2009 中,“Thread.Monitor”有额外的 4 字节,将总类大小增加到 16 字节。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-03-11
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多