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 到几千个元素。

假定从单个线程或使用外部同步机制从多个线程访问的代码。

如果字典在没有人使用它时是GCed的,我很好。我试图在时间(我想以某种方式缓存createDictionary的结果)和内存费用(我不想让内存占用超过必要的时间)之间找到平衡。

弱引用不是缓存的好解决方案,因为如果没有其他人引用您的字典,您的对象将无法在下一个 GC 中存活下来。您可以通过将创建的值存储在成员变量中来创建简单缓存,如果它不为 null,则重用它。

这不是线程安全的,如果您对字典具有大量并发访问权限,则在某些情况下最终会多次创建字典。您可以使用双重检查锁定模式来防止这种情况,同时将性能影响降至最低。

为了进一步帮助您,您需要指定并发访问是否是您的问题,以及字典消耗了多少内存以及如何创建它。例如,如果字典是昂贵查询的结果,则简单地将字典序列化为光盘并重用它可能会有所帮助,直到您需要重新创建它(这取决于您的特定需求)。

缓存

是内存泄漏的另一个词,如果您没有明确的策略何时应从缓存中删除对象。由于您正在尝试弱引用,因此我假设您不知道何时是清除缓存的好时机。

另一种选择是将字典压缩为较少占用内存的结构。您的字典有多少个键,值是什么?

有四种主要机制可供您使用(Lazy 在 4.0 中提供,因此没有选择)

  1. 延迟初始化
  2. 虚拟代理
  3. 价值持有者

每个都有自己的优势。

我建议使用一个值持有者,它在第一次调用 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 位进程中发生)。

最新更新