【问题标题】:Cache Function results缓存函数结果
【发布时间】:2010-11-13 02:14:21
【问题描述】:

为了好玩,我正在使用一个类来轻松缓存函数结果。基本思想是,您可以采用任何您想要的函数——尽管您只想将它​​用于相对昂贵的函数——并轻松地将其包装为使用相对便宜的字典查找,以便以后使用相同的参数运行。真的没什么:

public class AutoCache<TKey, TValue> 
{  
    public AutoCache(Func<TKey, TValue> FunctionToCache)
    {
        _StoredFunction = FunctionToCache;
        _CachedData = new Dictionary<TKey, TValue>();
    }

    public TValue GetResult(TKey Key)
    {
        if (!_CachedData.ContainsKey(Key)) 
            _CachedData.Add(Key, _StoredFunction(Key));
        return _CachedData[Key];
    }

    public void InvalidateKey(TKey Key)
    {
        _CachedData.Remove(Key);
    }

    public void InvalidateAll()
    {
        _CachedData.Clear();
    }

    private Dictionary<TKey, TValue> _CachedData;
    private Func<TKey, TValue> _StoredFunction; 
}

不幸的是,还有一些额外的限制使得它的用处大大降低。我们还可以在实现中添加一些功能和其他注意事项。我正在寻找有关以下任何方面的改进方法的想法:

  • 这需要一个针对给定参数集返回相同结果的函数(它必须是无状态的)。可能没有办法改变这一点。
  • 它仅限于非常狭窄的代表范围。我们是否可以将它扩展为轻松地为任何接受至少一个参数并返回一个值的函数工作,也许通过将参数包装在匿名类型中?或者我们是否需要为我们想要支持的每个 Func 委托额外实现?如果是这样,我们是否可以构建一个抽象类来简化此操作?
  • 它不是线程安全的。
  • 没有自动失效。这使得垃圾收集很危险。您需要将其保留一段时间以使其有用,这意味着您不会真正丢弃旧的和可能不需要的缓存项。
  • 对于函数只有一个参数的情况,我们能否继承它以使缓存双向?

作为参考,如果我曾经在实际代码中使用它,我认为它最有可能作为业务逻辑层的一部分,我使用此代码在数据访问层中包装一个方法,该方法只是拉取查找表中的数据。在这种情况下,相对于字典而言,数据库之旅的成本会很高,而且查找几乎总是只有一个“键”值,所以这是一个很好的匹配。

【问题讨论】:

    标签: c# .net caching


    【解决方案1】:

    这种自动缓存函数结果的另一个名称是记忆。对于公共接口,请考虑以下几点:

    public Func<T,TResult> Memoize<T,TResult>(Func<T,TResult> f)
    

    ...并简单地使用多态性将 T 存储在对象字典中。

    扩展委托范围可以通过柯里化和部分函数应用来实现。像这样的:

    static Func<T1,Func<T2,TResult>> Curry(Func<T1,T2,TResult> f)
    {
        return x => y => f(x, y);
    }
    // more versions of Curry
    

    由于Curry 将多个参数的函数转换为单个参数的函数(但可能返回函数),因此返回值本身可以用于记忆。

    另一种方法是使用反射来检查委托类型,并将元组存储在字典中,而不仅仅是参数类型。一个简单的元组只是一个数组包装器,其哈希码和相等逻辑使用深度比较和哈希。

    弱引用可以帮助失效,但是使用WeakReference 键创建字典很棘手——最好在运行时的支持下完成(弱引用值更容易)。我相信那里有一些实现。

    通过锁定突变事件的内部字典很容易实现线程安全,但是拥有无锁字典可能会提高高度并发场景中的性能。那个字典可能更难创建——不过有一个有趣的presentation on one for Java here

    【讨论】:

    • 很好,我会阅读更多关于记忆的内容。在这种情况下,我认为柯里化在这里并没有真正的帮助。是的,它可以使用此代码来处理具有多个参数的函数,但是它以一种对类用户来说并不明显的方式来处理,这违背了类的要点。
    【解决方案2】:

    哇——真是巧合——我最近刚刚发布了一个关于opaque keys in C# 的问题……因为我正在尝试实现一些与函数结果缓存相关的东西。好搞笑。

    这种类型的元编程在 C# 中可能会很棘手……尤其是因为泛型类型参数会导致尴尬的代码重复。你经常会在多个地方重复几乎相同的代码,使用不同的类型参数,以实现类型安全。

    所以这是我对您的方法的变体,它使用我的不透明键模式和闭包来创建可缓存的函数。下面的示例演示了带有一个或两个参数的模式,但它相对容易扩展到更多。它还使用扩展方法创建一个透明模式,使用 AsCacheable() 方法用可缓存的 Func 包装 Func。闭包捕获与函数关联的缓存 - 并使其对其他调用者透明。

    这种技术与您的方法有许多相同的限制(线程安全、保持引用等)——我怀疑它们并不难克服——但它确实支持一种简单的方法来扩展到多个参数,并且它允许缓存函数完全可以替换为常规函数——因为它们只是一个包装委托。

    还值得注意的是,如果您创建 CacheableFunction 的第二个实例 - 您将获得一个单独的缓存。这既可以是优势,也可以是劣势……因为在某些情况下您可能没有意识到这种情况正在发生。

    代码如下:

    public interface IFunctionCache
    {
        void InvalidateAll();
        // we could add more overloads here...
    }
    
    public static class Function
    {
        public class OpaqueKey<A, B>
        {
            private readonly object m_Key;
    
            public A First { get; private set; }
            public B Second { get; private set; }
    
            public OpaqueKey(A k1, B k2)
            {
                m_Key = new { K1 = k1, K2 = k2 };
                First = k1;
                Second = k2;
            }
    
            public override bool Equals(object obj)
            {
                var otherKey = obj as OpaqueKey<A, B>;
                return otherKey == null ? false : m_Key.Equals(otherKey.m_Key);
            }
    
            public override int GetHashCode()
            {
                return m_Key.GetHashCode();
            }
        }
    
        private class AutoCache<TArgs,TR> : IFunctionCache
        {
            private readonly Dictionary<TArgs,TR> m_CachedResults 
                = new Dictionary<TArgs, TR>();
    
            public bool IsCached( TArgs arg1 )
            {
                return m_CachedResults.ContainsKey( arg1 );
            }
    
            public TR AddCachedValue( TArgs arg1, TR value )
            {
                m_CachedResults.Add( arg1, value );
                return value;
            }
    
            public TR GetCachedValue( TArgs arg1 )
            {
                return m_CachedResults[arg1];
            }
    
            public void InvalidateAll()
            {
                m_CachedResults.Clear();
            }
        }
    
        public static Func<A,TR> AsCacheable<A,TR>( this Func<A,TR> function )
        {
            IFunctionCache ignored;
            return AsCacheable( function, out ignored );
        }
    
        public static Func<A, TR> AsCacheable<A, TR>( this Func<A, TR> function, out IFunctionCache cache)
        {
            var autocache = new AutoCache<A,TR>();
            cache = autocache;
            return (a => autocache.IsCached(a) ?
                         autocache.GetCachedValue(a) :
                         autocache.AddCachedValue(a, function(a)));
        }
    
        public static Func<A,B,TR> AsCacheable<A,B,TR>( this Func<A,B,TR> function )
        {
            IFunctionCache ignored;
            return AsCacheable(function, out ignored);
        }
    
        public static Func<A,B,TR> AsCacheable<A,B,TR>( this Func<A,B,TR> function, out IFunctionCache cache )
        {
            var autocache = new AutoCache<OpaqueKey<A, B>, TR>();
            cache = autocache;
            return ( a, b ) =>
                       {
                           var key = new OpaqueKey<A, B>( a, b );
                           return autocache.IsCached(key)
                                      ? autocache.GetCachedValue(key)
                                      : autocache.AddCachedValue(key, function(a, b));
                       };
        }
    }
    
    public class CacheableFunctionTests
    {
        public static void Main( string[] args )
        {
            Func<string, string> Reversal = s => new string( s.Reverse().ToArray() );
    
            var CacheableReverse = Reversal.AsCacheable();
    
            var reverse1 = CacheableReverse("Hello");
            var reverse2 = CacheableReverse("Hello"); // step through to prove it uses caching
    
            Func<int, int, double> Average = (a,b) => (a + b)/2.0;
            var CacheableAverage = Average.AsCacheable();
    
            var average1 = CacheableAverage(2, 4);
            var average2 = CacheableAverage(2, 4);
        }
    }
    

    【讨论】:

      【解决方案3】:

      因为这主要是为了教育价值 - 你应该看看 WeakReference 类,它允许 GC 在多线程环境中从你的类中清除未使用的句柄。这是 .NET 中非常常见的缓存模式

      也就是说 - 告售者!每个缓存都是不同的。通过构建一个包罗万象的解决方案,您通常会陷入一种病态的情况,即您的“缓存”只是一个美化的字典,其中包含许多复杂的辅助方法,使您的代码难以阅读。

      【讨论】:

        【解决方案4】:

        我正在使用这个简单的扩展,在这种情况下使用 MemoryCache:

        public static class FuncHelpers
        {
           /// <summary>
           /// Returns a same function wrapped into cache-mechanism
           /// </summary>
           public static Func<TIn, TRes> Cached<TIn, TRes>(this Func<TIn, TRes> func, 
              Func<TIn,string> keySelector, 
              Func<TIn,CacheItemPolicy> policy)
            {
                var cache = new MemoryCache(Guid.NewGuid().ToString());
        
                Func<TIn, TRes> f = (item) =>
                {
                    var key = keySelector(item);
                    var newItem = new Lazy<TRes>(() => func(item));
                    var oldItem = cache.AddOrGetExisting(key,newItem , policy(item)) as Lazy<TRes>;
                    try
                    {
                        return (oldItem ?? newItem).Value;
                    }
                    catch
                    {
                        // Handle cached lazy exception by evicting from cache.
                        cache.Remove(key);
                        throw;
                    }
        
                };
                return f;
            }
        
           //simplified version
           public static Func<TIn, TRes> Cached<TIn, TRes>(this Func<TIn, TRes> func, Func<TIn, string> keySelector,
                TimeSpan duration)
            {
                if (duration.Ticks<=0) return func;
                return Cached(func, keySelector,
                  item => new CacheItemPolicy() {AbsoluteExpiration = DateTimeOffset.Now + duration});
        
            }
        }
        

        示例/用法:(缓存持续时间为 42 秒):

            public class CachedCalculator
            {
                private Func<int, int> _heavyExpensiveMultiplier;
        
                public Calculator(Func<int,int> heavyExpensiveMultiplier )
                {
                    //wrap function into cached one
                    this._heavyExpensiveMultiplier 
                      = heavyExpensiveMultiplier.Cached(x =>/*key for cache*/ x.ToString(), TimeSpan.FromSeconds(42));
                }
        
                //this uses cached algorithm
                public int Compute(int x)
                {
                    return _heavyExpensiveMultiplier(x);
                }
            }
        

        【讨论】:

          猜你喜欢
          • 2011-01-14
          • 2021-10-05
          • 1970-01-01
          • 2019-09-01
          • 2017-04-29
          • 2011-01-18
          • 1970-01-01
          • 1970-01-01
          • 2012-03-06
          相关资源
          最近更新 更多