缓存计算值的方法

8
在我们正在开发的Delphi应用程序中,我们有一个包含大量相关对象的结构。这些对象的一些属性具有在运行时计算的值,我正在寻找一种缓存更加密集计算结果的方法。我采取的方法是在第一次计算时将值保存在私有成员中。以下是一个简短的示例代码:
unit Unit1;

interface

type
  TMyObject = class
  private
    FObject1, FObject2: TMyOtherObject;
    FMyCalculatedValue: Integer;
      function GetMyCalculatedValue: Integer;
  public
    property MyCalculatedValue: Integer read GetMyCalculatedValue;
  end;

implementation

  function TMyObject.GetMyCalculatedValue: Integer;
  begin
    if FMyCalculatedValue = 0 then
    begin
      FMyCalculatedValue :=
        FObject1.OtherCalculatedValue + // This is also calculated
        FObject2.OtherValue;
    end;

    Result := FMyCalculatedValue;
  end;

end.

计算所使用的对象发生变化时,缓存值应该被重置和重新计算并不罕见。到目前为止,我们通过使用观察者模式来解决这个问题:对象实现一个OnChange事件,以便其他对象可以订阅,当它们改变并重置缓存值时得到通知。这种方法有效,但也有一些缺点:
  • 管理订阅需要大量内存。
  • 当缓存值依赖于大量对象(例如列表)时,它的可扩展性不好。
  • 依赖关系不是非常具体(即使缓存值仅依赖于一个属性,当其他属性更改时它也会被重置)。
  • 管理订阅会影响整体性能,并且很难维护(对象被删除、移动等)。
  • 如何处理依赖于其他计算值的计算并不清楚。
最后的问题是:你能否提出其他实现缓存计算值的方法?

即使标记为“Delphi”,我也非常有兴趣知道是否已经开发出了特定的模式。 - Matthieu M.
我添加了 Delphi 标签,以便将建议限制为静态类型语言,而非垃圾回收语言。 - Tihauan
3个回答

4
如果你想避免观察者模式,可以尝试使用哈希方法。其思想是对参数进行“哈希”,并检查它是否与保存状态的“哈希”匹配。如果不匹配,则重新计算(因此将新哈希保存为键)。我知道这听起来像是我刚想到的,但实际上已被众所周知的软件使用。例如,Makefile替代品SCons 就使用它来检查目标是否需要重新构建,而不是使用时间戳方法。我们现在已经使用SCons一年多了,从未发现任何未重新构建目标的问题,所以他们的哈希方法很好用!

请确保计算哈希(或您选择的任何方法)比重新计算要快得多。 - Gerry Coll
是的,我没有指出它听起来很明显,但是像优化一样...你真的必须进行测量。 - Matthieu M.

2
您可以存储所需的外部对象值的本地副本。然后,访问例程将比较本地副本和外部值,并仅在更改时进行重新计算。
类似地,访问外部对象属性也可能会强制重新评估这些属性,因此系统应自动保持最新状态,但仅在需要时重新计算。我不知道您是否需要采取措施避免循环依赖。
这会增加每个对象所需的空间量,但会消除观察者模式。它还推迟了所有计算,直到需要执行计算,而不是在每次源参数更改时都执行计算。希望这对您的系统有所帮助。
unit Unit1;

interface

type
  TMyObject = class
  private
    FObject1, FObject2: TMyOtherObject;
    FObject1Val, FObject2Val: Integer;
    FMyCalculatedValue: Integer;
      function GetMyCalculatedValue: Integer;
  public
    property MyCalculatedValue: Integer read GetMyCalculatedValue;
  end;

implementation

  function TMyObject.GetMyCalculatedValue: Integer;
  begin
    if (FObject1.OtherCalculatedValue <> FObjectVal1)
    or (FObject2.OtherValue <> FObjectVal2) then
    begin
      FMyCalculatedValue :=
        FObject1.OtherCalculatedValue + // This is also calculated
        FObject2.OtherValue;
      FObjectVal1 := FObject1.OtherCalculatedValue;
      FObjectVal2 := Object2.OtherValue;
    end;

    Result := FMyCalculatedValue;
  end;

end.

我也会验证在执行计算之前是否已分配fObject1和fObject2...只是为了安全起见。 - skamradt
@skamradt:同意。我假设问题中省略了输入验证/错误检测,以便保持示例代码的简洁性。 - IanH

1
在我的工作中,我使用DelphiBold,它可以管理相互依赖的无限复杂结构的缓存值。通常每个变量只保存问题的一小部分,在这个框架中被称为派生属性。由于该值未保存在数据库中,因此它仅取决于其他派生属性或数据库中的持久属性。
这种属性的代码是用Delphi编写的过程或者在模型中用OCL(对象约束语言)编写的。如果您将其编写为Delphi代码,则必须订阅相关变量。因此,如果属性C依赖于A和B,则每当A或B更改时,重新计算C的代码会在读取C时自动调用。因此,第一次读取C时也会读取A和B(可能来自数据库)。只要A和B没有更改,就可以读取C并获得非常快的性能。对于复杂的计算,这可以节省相当多的CPU时间。
不幸的消息是,Bold不再得到官方支持,也无法购买。我想如果你问足够多的人,你可能会得到它,但我不知道你可以从哪里下载它。大约在2005-2006年,它可以从Borland免费下载,但现在不行了。它还没有准备好支持D2009,因为有人必须将其移植到Unicode。

另一个选择是使用来自Capable Objects的dot.net的ECO。ECO是Visual Studio中的插件。它是一个受支持的框架,与Delphi的Bold具有相同的思想和作者。许多东西也得到了改进,例如数据绑定用于GUI组件。Bold和ECO都使用模型作为中心点,包括类、属性和链接。这些可以持久化在数据库或xml文件中。使用ECO的免费版本,模型最多可以有12个类,但我记得没有其他限制。

Bold和ECO包含的不仅仅是派生属性,这使您更加高效,并允许您考虑问题而不是数据库的技术细节,或者在您的情况下如何缓存值。欢迎您就这些框架提出更多问题!

编辑: 实际上,对于D7,Embarcadero注册用户有一个下载链接可以下载Bold for Delphi,这是相当古老的版本... 我知道有更新版本适用于D2005和D2006。


基于模型的框架总体而言,特别是“Bold for Delphi”听起来非常有趣。谢谢! - Tihauan
我在我的一个硬盘上找到了D2006的Bold版本(我认为这是最新的公共版本),如果你有兴趣,可以通过电子邮件联系我:roland.bengtsson@gmail.com,我会发送给你。或者通过Skype联系我,我的ID是d98rolb。 - Roland Bengtsson
Tihauan,有关派生值的更多信息和一个小例子,请查看我的博客http://boldfordelphi.blogspot.com/#derattr。 - Roland Bengtsson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接