[英]Memory management / caching for costly objects in C#
假設我有以下對象
public class MyClass
{
public ReadOnlyDictionary<T, V> Dict
{
get
{
return createDictionary();
}
}
}
假設ReadOnlyDictionary
是Dictionary<T, V>
的只讀包裝器。
createDictionary
方法需要很長時間才能完成,並且返回的字典相對較大。
顯然,我想實現某種緩存,所以我可以重用createDictionary
結果,但我也不想濫用垃圾收集器並使用大量內存。
我想過將WeakReference
用於字典,但不確定這是否是最佳方法。
你會推薦什么? 如何正確處理可能被多次調用的昂貴方法的結果?
更新:
我對C#2.0庫(單個DLL,非可視化)的建議感興趣。 該庫可能在Web應用程序的桌面中使用。
更新2:
該問題也適用於只讀對象。 我將屬性的值從Dictionary
更改為ReadOnlyDictionary
。
更新3:
T
是相對簡單的類型(例如,字符串)。 V
是一個自定義類。 您可能認為V
的實例創建成本很高。 字典可能包含0到數千個元素。
假定代碼從單個線程或具有外部同步機制的多個線程訪問。
如果沒有人使用字典,那么我很好。 我試圖在時間(我想以某種方式緩存createDictionary
的結果)和內存開銷之間找到平衡(我不想讓內存占用超過必要的時間)。
WeakReference對於緩存來說不是一個好的解決方案,因為如果沒有其他人引用你的字典,你的對象將無法在下一個GC中存活。 您可以通過將創建的值存儲在成員變量中來創建一個簡單的緩存,如果它不為null,則重用它。
這不是線程安全的,如果你有很多可靠的訪問權限,你最終會在某些情況下創建字典幾次。 您可以使用雙重檢查的鎖定模式來防止這種情況,同時將沖擊力降至最小。
為了進一步幫助您,您需要指定並發訪問是否是您的問題,以及您的字典消耗了多少內存以及如何創建它。 如果例如字典是昂貴查詢的結果,則可能有助於簡單地將字典序列化為光盤並重復使用它,直到您需要重新創建它(這取決於您的特定需求)。
如果在從緩存中刪除對象時沒有明確的策略,則緩存是內存泄漏的另一個詞。 由於您正在嘗試WeakReference,我假設您不知道何時清除緩存的確定時間。
另一個選擇是將字典壓縮為內存較少的內存。 你的詞典有多少個鍵,值是多少?
有四種主要機制可供您使用(Lazy有4.0,所以沒有選擇)
每個人都有自己的優勢。
我建議一個值持有者,它在第一次調用持有者的GetValue方法時填充字典。 然后你可以使用該值,只要你想和它只做一次它只在需要時才完成。
有關更多信息,請參閱martin fowlers頁面
你確定需要緩存整個字典嗎?
根據您的說法,保留最近使用的鍵值對列表可能會更好。
如果在列表中找到密鑰,則只返回該值。
如果不是,則創建一個值(假設比創建所有值更快,並且使用更少的內存)並將其存儲在列表中,從而刪除未使用時間最長的鍵值對。
這是一個非常簡單的MRU列表實現,它可以作為靈感:
using System.Collections.Generic;
using System.Linq;
internal sealed class MostRecentlyUsedList<T> : IEnumerable<T>
{
private readonly List<T> items;
private readonly int maxCount;
public MostRecentlyUsedList(int maxCount, IEnumerable<T> initialData)
: this(maxCount)
{
this.items.AddRange(initialData.Take(maxCount));
}
public MostRecentlyUsedList(int maxCount)
{
this.maxCount = maxCount;
this.items = new List<T>(maxCount);
}
/// <summary>
/// Adds an item to the top of the most recently used list.
/// </summary>
/// <param name="item">The item to add.</param>
/// <returns><c>true</c> if the list was updated, <c>false</c> otherwise.</returns>
public bool Add(T item)
{
int index = this.items.IndexOf(item);
if (index != 0)
{
// item is not already the first in the list
if (index > 0)
{
// item is in the list, but not in the first position
this.items.RemoveAt(index);
}
else if (this.items.Count >= this.maxCount)
{
// item is not in the list, and the list is full already
this.items.RemoveAt(this.items.Count - 1);
}
this.items.Insert(0, item);
return true;
}
else
{
return false;
}
}
public IEnumerator<T> GetEnumerator()
{
return this.items.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
在您的情況下,T是鍵值對。 保持maxcount足夠小,以便搜索保持快速,並避免過多的內存使用。 每次使用項目時調用添加。
如果對象在緩存中的存在的有用生命周期與對象的引用生命周期相當,則應用程序應使用WeakReference
作為緩存機制。 例如,假設您有一個方法將基於反序列化String
創建ReadOnlyDictionary
。 如果常見的使用模式是讀取字符串,創建一個字典,用它做一些事情,放棄它,然后再用另一個字符串重新開始, WeakReference
可能不太理想。 另一方面,如果您的目標是將許多字符串(其中相當一部分將相同)反序列化為ReadOnlyDictionary
實例,則重復嘗試反序列化相同的字符串會產生相同的實例可能非常有用。 請注意,節省的不僅僅是因為一個人只需要做一次構建實例的工作,而且還來自以下事實:(1)沒有必要在內存中保留多個實例,以及(2)如果ReadOnlyDictionary
變量引用相同的實例,則可以知道它們是等效的,而不必檢查實例本身。 相反,確定兩個不同的ReadOnlyDictionary
實例是否相同可能需要檢查每個實例中的所有項。 必須進行許多此類比較的代碼可以從使用WeakReference
緩存中受益,以便包含等效實例的變量通常包含相同的實例。
我認為您有兩種機制可以依賴緩存,而不是開發自己的機制。 第一個,正如您自己建議的那樣,是使用WeakReference,並讓垃圾收集器決定何時釋放這個內存。
你有第二種機制 - 內存分頁。 如果字典是一次性創建的,它可能會存儲在堆的或多或少的連續部分中。 只需保持字典存活,如果您不需要,讓Windows將其分頁到交換文件中。 根據您的使用情況(您的詞典訪問的隨機性),您最終可能會獲得比WeakReference更好的性能。
如果您接近地址空間限制(這種情況僅發生在32位進程中),則第二種方法會出現問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.