函數語言程式設計-記憶化快取

溫暖如太陽發表於2021-01-01

記憶化,是一種為了提高應用程式效能的FP技術。程式加速是通過快取函式的結果實現的,避免了重複計算帶來的額外開銷。

1、現在我們使用Dictionary作為快取結構

 1 public static Func<T, R> Memoize<T, R>(Func<T, R> func) 
 2     where T : IComparable
 3 {
 4     Dictionary<T, R> cache = new Dictionary<T, R>();
 5     return arg =>
 6     {
 7         if (cache.ContainsKey(arg))
 8             return cache[arg];
 9         return (cache[arg] = func(arg));
10     };
11 }
1 public static string GetString(string name)
2 {
3     return $"return date {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")} string {name}";
4 }
1 var getStrMemoize = Memoize<string, string>(GetString);
2 Console.WriteLine(getStrMemoize("A"));
3 Thread.Sleep(3000);
4 Console.WriteLine(getStrMemoize("B"));
5 Thread.Sleep(3000);
6 Console.WriteLine(getStrMemoize("A"));

列印結果:

1 return date 2020-12-31 08:37:12 string A
2 return date 2020-12-31 08:37:15 string B
3 return date 2020-12-31 08:37:12 string A

可以看出第三次列印的結果跟第一次列印的結果相同,也就是被快取在Dictionary中的值。

在單執行緒中我們這樣寫沒有問題,程式順序被執行,Dictionary不存在併發問題,但是當我們想在多個執行緒並行時Dictionary不是執行緒安全集合,會存線上程安全問題。

2、現在我們使用執行緒安全集合ConcurrentDictionary進行改進:(方法中註釋已經對方法做了說明,在此不重複)

 1 /// <summary>
 2 /// 使用執行緒安全集合
 3 /// </summary>
 4 /// <typeparam name="T"></typeparam>
 5 /// <typeparam name="R"></typeparam>
 6 /// <param name="func"></param>
 7 /// 對於字典的修改和寫入操作, ConcurrentDictionary<TKey,TValue> 使用細粒度鎖定以確保執行緒安全。
 8 /// 對字典進行 (讀取操作時,將以無鎖方式執行。) 不過,在 valueFactory 鎖的外部呼叫委託,以避免在鎖定下執行未知程式碼時可能產生的問題。
 9 /// 因此,對於 GetOrAdd 類上的所有其他操作而言,不是原子的 ConcurrentDictionary<TKey,TValue>10 /// 由於在生成值時,另一個執行緒可以插入鍵/值 valueFactory ,因此您不能信任這一點,
11 /// 因為已 valueFactory 執行,其生成的值將插入到字典中並返回。
12 /// 如果 GetOrAdd 在不同的執行緒上同時呼叫,則 valueFactory 可以多次呼叫,但只會將一個鍵/值對新增到字典中。
13 /// 返回值取決於字典中的鍵是否存在,以及是否在 GetOrAdd 呼叫之後但在生成值之前由另一個執行緒插入了鍵/值 valueFactory
14 /// (如果當前執行緒檢查到Key不在字典中,那麼會執行生成鍵值;但是在寫入前如果有執行緒完成了寫入鍵值,當前執行緒寫入前檢查到有寫入值,則以已寫入的為準)。
15 /// <returns></returns>
16 public static Func<T, R> MemoizeThreadSafe<T, R>(Func<T, R> func) where T : IComparable
17 {
18     ConcurrentDictionary<T, R> cache = new ConcurrentDictionary<T, R>();
19     return arg =>
20     {
21         return cache.GetOrAdd(arg, a => func(arg));
22     };
23 }
1 var getStrMemoize = MemoizeThreadSafe<string, string>(GetString);
2 Console.WriteLine(getStrMemoize("A"));
3 Thread.Sleep(3000);
4 Console.WriteLine(getStrMemoize("B"));
5 Thread.Sleep(3000);
6 Console.WriteLine(getStrMemoize("A"));

列印結果:

1 return date 2020-12-31 08:42:46 string A
2 return date 2020-12-31 08:42:49 string B
3 return date 2020-12-31 08:42:46 string A

註解中我們說明了ConcurrentDictionary是執行緒安全集合,但是當我們使用GetOrAdd時,由於該方法不是原子性的操作,當進行初始化時,可能多個執行緒同時進行初始化操作,帶來了額外的開銷。

3、為解決GetOrAdd非原子性操作重複初始化操作,引入延遲初始化(註解已詳細說明):

在看改進方法前我們先看下Lazy類的用法:

1 public class user
2 {
3     public string name { get; set; }
4 }
1 Lazy<user> user = new Lazy<user>();
2 if (!user.IsValueCreated)
3     Console.WriteLine("user 未建立.");
4 user.Value.name = "test";
5 if (user.IsValueCreated)
6     Console.WriteLine("user 已建立.");

輸出:

1 user 未建立.
2 user 已建立.

以下為Lazy類程式碼片段,從程式碼我們看出在物件未使用(value)前,例項並未真正建立:

 1 [NonSerialized]
 2 private Func<T> m_valueFactory;
 3 
 4 private object m_boxed;
 5 
 6 public T Value
 7 {
 8     get
 9     {
10         return LazyInitValue();
11     }
12 }
13 private T LazyInitValue()
14 {
15     Boxed boxed = null;
16     try
17     {
18         boxed = CreateValue();
19         m_boxed = boxed;
20     }
21     finally
22     {
23     }
24     return boxed.m_value;
25 }
26 
27 private Boxed CreateValue()
28 {
29     Boxed boxed = null;
30     if (m_valueFactory != null) //() => func(arg)
31     {
32         try
33         {
34             Func<T> factory = m_valueFactory;
35 
36             boxed = new Boxed(factory());
37         }
38         catch (Exception ex)
39         {
40             throw;
41         }
42     }
43 
44 
45     return boxed;
46 }
47 
48 [Serializable]
49 class Boxed
50 {
51     internal Boxed(T value)
52     {
53         m_value = value;
54     }
55     internal T m_value;
56 }

現在我們看下改進方法:

 1 /// <summary>
 2 /// 為解決GetOrAdd 非原子性操作,
 3 /// 重複初始化操作,引入Lazy型別、
 4 /// 延遲初始化
 5 /// </summary>
 6 /// <typeparam name="T"></typeparam>
 7 /// <typeparam name="R"></typeparam>
 8 /// <param name="func"></param>
 9 /// 使用延遲初始化來延遲建立大型或消耗大量資源的物件,或者執行大量佔用資源的任務
10 /// ,尤其是在程式的生存期內可能不會發生這種建立或執行時。
11 /// 若要為遲緩初始化做好準備,請建立的例項 Lazy<T>12 /// 你建立的物件的型別引數 Lazy<T> 指定你希望延遲初始化的物件的型別。
13 /// 用於建立物件的建構函式 Lazy<T> 確定初始化的特徵。
14 /// 首次訪問 Lazy<T>.Value 屬性時出現延遲初始化。
15 /// <returns></returns>
16 public static Func<T, R> MemoizeLazyThreadSafe<T, R>(Func<T, R> func) where T : IComparable
17 {
18   ConcurrentDictionary<T, Lazy<R>> cache = new ConcurrentDictionary<T, Lazy<R>>();
19   return arg =>
20   {
21       return cache.GetOrAdd(arg, a => new Lazy<R>(() => func(arg))).Value;
22   };
23 }

到現在方法的執行緒安全、初始化載入問題都解決了,但是我們在解決重複計算的問題後卻又不得不考慮快取帶來的記憶體損耗問題。我們例項化了ConcurrentDictionary物件,並且該物件作為強引用型別一直未被釋放,那麼GC是無法回收該物件,帶來的問題是記憶體一直被佔用,隨著方法引用次數越來越多記憶體開銷則會越來越大。

4、為解決該問題,我們引入過期時間,根據過期時間釋放快取值。

 1 public static Func<T, R> MemoizeWeakWithTtl<T, R>(Func<T, R> func, TimeSpan ttl)
 2     where T : class, IEquatable<T>
 3     where R : class
 4 {
 5     var keyStore = new ConcurrentDictionary<int, T>();
 6 
 7     T ReduceKey(T obj)
 8     {
 9         var oldObj = keyStore.GetOrAdd(obj.GetHashCode(), obj);
10         return obj.Equals(oldObj) ? oldObj : obj;
11     }
12 
13     var cache = new ConditionalWeakTable<T, Tuple<R, DateTime>>();
14 
15     Tuple<R, DateTime> FactoryFunc(T key) =>
16         new Tuple<R, DateTime>(func(key), DateTime.Now + ttl);
17 
18     return arg =>
19     {
20         var key = ReduceKey(arg);
21         var value = cache.GetValue(key, FactoryFunc);
22         if (value.Item2 >= DateTime.Now)
23             return value.Item1;
24         value = FactoryFunc(key);
25         cache.Remove(key);
26         cache.Add(key, value);
27         return value.Item1;
28     };
29 }

其他實現方式,使用WeakReference弱引用型別(以下為使用示例):

 1 public class Cache
 2 {
 3     static Dictionary<int, WeakReference> _cache;
 4 
 5     int regenCount = 0;
 6 
 7     public Cache(int count)
 8     {
 9         _cache = new Dictionary<int, WeakReference>();
10 
11         for (int i = 0; i < count; i++)
12         {
13             _cache.Add(i, new WeakReference(new Data(i), false));
14         }
15     }
16 
17     public int Count
18     {
19         get { return _cache.Count; }
20     }
21 
22     public int RegenerationCount
23     {
24         get { return regenCount; }
25     }
26 
27     public Data this[int index]
28     {
29         get
30         {
31             Data d = _cache[index].Target as Data;
32             if (d == null)
33             {
34                 Console.WriteLine("Regenerate object at {0}: Yes", index);
35                 d = new Data(index);
36                 _cache[index].Target = d;
37                 regenCount++;
38             }
39             else
40             {
41                 Console.WriteLine("Regenerate object at {0}: No", index);
42             }
43 
44             return d;
45         }
46     }
47 }
48 
49 
50 public class Data
51 {
52     private byte[] _data;
53     private string _name;
54 
55     public Data(int size)
56     {
57         _data = new byte[size * 1024];
58         _name = size.ToString();
59     }
60 
61     // Simple property.
62     public string Name
63     {
64         get { return _name; }
65     }
66 }
 1 int cacheSize = 50;
 2 Random r = new Random();
 3 Cache c = new Cache(cacheSize);
 4 
 5 string DataName = "";
 6 GC.Collect(0);
 7 
 8 for (int i = 0; i < c.Count; i++)
 9 {
10     int index = r.Next(c.Count);
11     DataName = c[index].Name;
12 }
13 double regenPercent = c.RegenerationCount / (double)c.Count;
14 Console.WriteLine("Cache size: {0}, Regenerated: {1:P2}%", c.Count, regenPercent);

列印結果:

 1 Regenerate object at 46: Yes
 2 Regenerate object at 5: Yes
 3 Regenerate object at 6: Yes
 4 Regenerate object at 31: Yes
 5 Regenerate object at 1: Yes
 6 Regenerate object at 33: Yes
 7 Regenerate object at 11: Yes
 8 Regenerate object at 5: No
 9 Regenerate object at 37: Yes
10 Regenerate object at 15: Yes
11 Regenerate object at 25: Yes
12 Regenerate object at 14: No
13 Regenerate object at 16: Yes
14 Regenerate object at 20: Yes
15 Regenerate object at 10: Yes
16 Regenerate object at 14: No
17 Regenerate object at 17: Yes
18 Regenerate object at 28: Yes
19 Regenerate object at 7: Yes
20 Regenerate object at 34: Yes
21 Regenerate object at 45: Yes
22 Regenerate object at 33: No
23 Regenerate object at 29: Yes
24 Regenerate object at 32: Yes
25 Regenerate object at 32: No
26 Regenerate object at 4: No
27 Regenerate object at 42: Yes
28 Regenerate object at 6: No
29 Regenerate object at 16: No
30 Regenerate object at 36: Yes
31 Regenerate object at 12: Yes
32 Regenerate object at 9: Yes
33 Regenerate object at 43: Yes
34 Regenerate object at 12: No
35 Regenerate object at 49: Yes
36 Regenerate object at 37: No
37 Regenerate object at 36: No
38 Regenerate object at 44: Yes
39 Regenerate object at 22: Yes
40 Regenerate object at 31: No
41 Regenerate object at 1: No
42 Regenerate object at 24: No
43 Regenerate object at 23: Yes
44 Regenerate object at 38: Yes
45 Regenerate object at 6: No
46 Regenerate object at 31: No
47 Regenerate object at 28: No
48 Cache size: 50, Regenerated: 66.00%%

具體實現方式不在此實現。

 

相關文章