【问题标题】:Whose responsibility is it to cache / memoize function results?缓存/记忆函数结果是谁的责任?
【发布时间】:2023-03-30 17:19:01
【问题描述】:

我正在开发允许用户通过实现一组接口来扩展系统的软件。

为了测试我们所做工作的可行性,我的公司“吃自己的狗粮”,方法是在这些类中以与用户完全相同的方式实现我们所有的业务逻辑.

我们有一些实用程序类/方法将所有内容联系在一起并使用可扩展类中定义的逻辑。


我想缓存用户定义函数的结果。我应该在哪里执行此操作?

  • 是类本身吗?这似乎会导致大量代码重复。

  • 是使用这些类的实用程序/引擎吗?如果是这样,不知情的用户可能会直接调用类函数而不会获得任何缓存优势。


示例代码

public interface ILetter { string[] GetAnimalsThatStartWithMe(); }

public class A : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Aardvark", "Ant" }; 
                           }
                         }
public class B : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Baboon", "Banshee" }; 
                           } 
                         }
/* ...Left to user to define... */
public class Z : ILetter { public string[] GetAnimalsThatStartWithMe()
                           { 
                               return new [] { "Zebra" };
                           }
                         }

public static class LetterUtility
{
    public static string[] GetAnimalsThatStartWithLetter(char letter)
    {
        if(letter == 'A') return (new A()).GetAnimalsThatStartWithMe();
        if(letter == 'B') return (new B()).GetAnimalsThatStartWithMe();
        /* ... */
        if(letter == 'Z') return (new Z()).GetAnimalsThatStartWithMe();
        throw new ApplicationException("Letter " + letter + " not found");
    }
}

LetterUtility 应该负责缓存吗? ILetter 的每个单独实例都应该吗?还有其他完全可以做的事情吗?

我试图使这个示例简短,因此这些示例函数不需要缓存。但是考虑一下我添加了这个类,它使(new C()).GetAnimalsThatStartWithMe() 每次运行都需要 10 秒:

public class C : ILetter
{
    public string[] GetAnimalsThatStartWithMe()
    {
        Thread.Sleep(10000);
        return new [] { "Cat", "Capybara", "Clam" };
    }
}

我发现自己在使我们的软件尽可能快和维护更少的代码(在本例中:将结果缓存在 LetterUtility)和一遍又一遍地执行完全相同的工作(在本例中:每次等待 10 秒使用时间C)。

【问题讨论】:

  • 如果用户实现的接口具有引用不透明的函数,因此不可记忆怎么办?这不会破坏这个吗?我想这表明缓存应该由实现类来完成,因为它,或者至少它的编写者,知道它的函数是否是引用透明的,或者什么时候需要缓存。
  • @Sean 是的,它似乎会。我可能不得不告诉每个字母“你自己来缓存”,但这听起来像是我(作为框架维护者)实现的每个 ILetter 的大量复制/粘贴代码。
  • 为缓存行为本身创建一个接口(IMemoizer?),并为您自己的缓存目的实现它。然后有一个 ILetter 的默认实现(它可以是抽象的、内部的等),您知道可以正确使用您的默认 IMemoizer。此外,在 ILetter 接口上放置一个返回 IMemoizer 接口的属性或方法,另外可能还有一个像“IsMemoizable”这样的标志,因此 API 用户必须意识到它们是自己进行缓存的事实。然后他们可以实现自己的 IMemoizer。类似的东西?

标签: c# caching responsibility


【解决方案1】:

哪一层最负责缓存这些用户可定义函数的结果?

答案很明显:能够正确实现所需缓存策略的层就是正确的层。

正确的缓存策略需要具备两个特征:

  • 它绝不能提供过时的数据;它必须知道被缓存的方法是否会产生不同的结果,并在调用者获得陈旧数据之前的某个时间点使缓存无效

  • 它必须代表用户有效地管理缓存资源。没有过期策略且无限增长的缓存还有另一个名称:我们通常称其为“内存泄漏”。

您系统中的哪个层知道“缓存是否陈旧”问题的答案?和“缓存太大了吗?” 这是应该实现缓存的层。

【讨论】:

  • 有时陈旧的数据是好的,但这增加了一个问题,“哪一层可以决定多少陈旧是可以接受的?”
  • @JonHanna:当然。只需将“它绝不能提供陈旧数据”替换为“它绝不能提供过度陈旧数据”。正如您所说,那么问题就变成了“哪一层知道数据何时过时?”
  • 我只是觉得那个变种很有趣:)
【解决方案2】:

像缓存这样的东西可以被认为是一个“跨领域”问题(http://en.wikipedia.org/wiki/Cross-cutting_concern):

在计算机科学中,横切关注点是程序中影响其他关注点的方面。在设计和实现中,这些问题通常无法从系统的其余部分中彻底分解,并且可能导致分散(代码重复)、缠结(系统之间的重要依赖关系)或两者兼而有之。 例如,如果编写一个用于处理医疗记录的应用程序,那么这些记录的簿记和索引是核心问题,而将更改历史记录到记录数据库或用户数据库或身份验证系统将是横切关注点,因为它们涉及程序的更多部分。

横切关注点通常可以通过面向方面的编程 (http://en.wikipedia.org/wiki/Aspect-oriented_programming) 来实现。

在计算中,面向方面编程 (AOP) 是一种编程范式,旨在通过允许分离横切关注点来增加模块化。 AOP 构成了面向方面的软件开发的基础。

.NET 中有许多工具可以促进面向方面的编程。我最喜欢那些提供完全透明实现的。以缓存为例:

public class Foo
{
    [Cache(10)] // cache for 10 minutes
    public virtual void Bar() { ... }
}

这就是你需要做的一切......通过定义如下行为,其他一切都会自动发生:

public class CachingBehavior
{
   public void Intercept(IInvocation invocation) { ... } 
   // this method intercepts any method invocations on methods attributed with the [Cache] attribute. 
  // In the case of caching, this method would check if some cache store contains the data, and if it does return it...else perform the normal method operation and store the result
}

对于这种情况的发生有两个一般学校:

  1. 构建后 IL 编织。 PostSharp、Microsoft CCI 和 Mono Cecil 等工具可以配置为自动重写这些属性化方法,以自动委托给您的行为。

  2. 运行时代理。 Castle DynamicProxy 和 Microsoft Unity 等工具可以自动生成代理类型(从 Foo 派生的类型,在上面的示例中覆盖 Bar),以委托给您的行为。

【讨论】:

    【解决方案3】:

    虽然我不懂 C#,但这似乎是使用 AOP(面向方面​​编程)的一个案例。这个想法是您可以“注入”要在执行堆栈中的某些点执行的代码。

    可以如下添加缓存代码:

    IF( InCache( object, method, method_arguments ) )
      RETURN Cache(object, method, method_arguments);
    ELSE
      ExecuteMethod(); StoreResultsInCache();
    

    然后您定义此代码应在每次调用您的接口函数(以及实现这些函数的所有子类)之前执行。

    能否请 .NET 专家告诉我们如何在 .NET 中执行此操作?

    【讨论】:

    • 周围有大量的 .NET 工具来支持 AOP 编程。它们的实现范围从构建后 CIL 重写(编织)到动态代理生成(其中代理覆盖虚拟或接口方法)。候选人包括 Castle Windsor/DynamicProxy、MS Unity、PostSharp、Mono Cecil 等等。最终结果基本上就是你上面描述的布尔逻辑。
    【解决方案4】:

    一般来说,缓存和记忆在以下情况下是有意义的:

    1. 获取结果是(或至少可能是)高延迟或比缓存本身造成的费用昂贵。
    2. 结果具有查找模式,其中将频繁调用函数的相同输入(即,不仅是参数,还包括影响结果的任何实例、静态数据和其他数据)。
    3. 相关代码调用的代码中没有现有的缓存机制,因此没有必要这样做。
    4. 在调用相关代码的代码中不会有另一个缓存机制,这使得这变得不必要(为什么在该方法中记住 GetHashCode() 几乎没有意义,尽管人们经常想在何时实现相对昂贵)。
    5. 不可能过时,在加载缓存时不太可能过时,如果过时或过时很容易检测到,则无关紧要。

    在某些情况下,组件的每个用例都会匹配所有这些。还有更多他们不会的地方。例如,如果一个组件缓存了结果,但从未被特定客户端组件以相同的输入调用两次,那么缓存只是一种浪费,对性能产生了负面影响(可能可以忽略不计,也可能很严重)。

    通常情况下,客户端代码决定适合它的缓存策略更有意义。面对现实世界的数据,此时针对特定用途进行调整通常也比在组件中更容易(因为它所面临的现实世界数据可能因用途而异)。

    更难知道什么程度的陈旧是可以接受的。通常,一个组件必须假设它需要 100% 的新鲜度,而客户端组件可以知道一定程度的陈旧性就可以了。

    另一方面,组件更容易获得对缓存有用的信息。在这些情况下,组件可以协同工作,尽管涉及更多(例如 RESTful Web 服务使用的 If-Modified-Since 机制,其中服务器可以指示客户端可以安全地使用它已缓存的信息)。

    此外,组件可以具有可配置的缓存策略。连接池是一种缓存策略,考虑一下它是如何配置的。

    总之:

    可以确定哪些缓存既可行又有用的组件。

    这通常是客户端代码。尽管组件作者记录的可能延迟和陈旧的详细信息在这里会有所帮助。

    在组件的帮助下,客户端代码很少出现,但您必须公开缓存的详细信息以允许这样做。

    并且有时可以是调用代码可配置缓存策略的组件。

    很少能成为组件,因为所有可能的用例都很少被相同的缓存策略很好地服务。一个重要的例外是该组件的同一个实例将为多个客户端提供服务,因为影响上述情况的因素分布在这些多个客户端上。

    【讨论】:

      【解决方案5】:

      以前的所有帖子都提出了一些好的观点,这里是一个非常粗略的概述,您可以这样做。我是即时写的,所以可能需要一些调整:

      interface IMemoizer<T, R>
      {
         bool IsValid(T args); //Is the cache valid, or stale, etc. 
         bool TryLookup(T args, out R result);    
         void StoreResult(T args, R result); 
      }
      
      static IMemoizerExtensions
      {
         Func<T, R> Memoizing<T, R>(this IMemoizer src, Func<T, R> method)
         {
            return new Func<T, R>(args =>
            {
               R result;
      
               if (src.TryLookup(args, result) && src.IsValid(args))
               {
                  return result; 
               }
               else
               {
                  result = method.Invoke(args); 
                  memoizer.StoreResult(args, result); 
                  return result; 
               }
            }); 
         }   
      }
      

      【讨论】:

        猜你喜欢
        • 2014-04-11
        • 2023-03-29
        • 2021-11-24
        • 2017-06-27
        • 2012-09-05
        • 2012-04-20
        • 1970-01-01
        • 2010-11-13
        • 2016-06-27
        相关资源
        最近更新 更多