C# 中昂贵对象的内存管理/缓存
本文关键字:内存 管理 缓存 对象 | 更新日期: 2023-09-27 18:36:39
>假设我有以下对象
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 到几千个元素。
假定从单个线程或使用外部同步机制从多个线程访问的代码。
如果字典在没有人使用它时是GCed的,我很好。我试图在时间(我想以某种方式缓存createDictionary
的结果)和内存费用(我不想让内存占用超过必要的时间)之间找到平衡。
弱引用不是缓存的好解决方案,因为如果没有其他人引用您的字典,您的对象将无法在下一个 GC 中存活下来。您可以通过将创建的值存储在成员变量中来创建简单缓存,如果它不为 null,则重用它。
这不是线程安全的,如果您对字典具有大量并发访问权限,则在某些情况下最终会多次创建字典。您可以使用双重检查锁定模式来防止这种情况,同时将性能影响降至最低。
为了进一步帮助您,您需要指定并发访问是否是您的问题,以及字典消耗了多少内存以及如何创建它。例如,如果字典是昂贵查询的结果,则简单地将字典序列化为光盘并重用它可能会有所帮助,直到您需要重新创建它(这取决于您的特定需求)。
缓存是内存泄漏的另一个词,如果您没有明确的策略何时应从缓存中删除对象。由于您正在尝试弱引用,因此我假设您不知道何时是清除缓存的好时机。
另一种选择是将字典压缩为较少占用内存的结构。您的字典有多少个键,值是什么?
有四种主要机制可供您使用(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
缓存中受益,以便保存等效实例的变量通常保存相同的实例。
我认为您可以依靠两种机制进行缓存,而不是开发自己的机制。第一个,正如你自己建议的,是使用弱引用,并让垃圾回收器决定何时释放此内存。
您有第二种机制 - 内存分页。如果字典是一举创建的,它可能会存储在堆的或多或少连续部分中。只需保持字典处于活动状态,如果您不需要它,请让 Windows 将其分页到交换文件中。根据您的使用情况(字典访问的随机性),您最终可能会获得比弱引用更好的性能。
如果接近地址空间限制,则第二种方法存在问题(仅在 32 位进程中发生)。