6

假设我有以下对象

public class MyClass
{
    public ReadOnlyDictionary<T, V> Dict
    {
        get
        {
            return createDictionary();
        }
    }
}

假设这ReadOnlyDictionary是一个只读包装器Dictionary<T, V>

createDictionary方法需要大量时间才能完成,并且返回的字典相对较大。

显然,我想实现某种缓存,这样我就可以重用结果,createDictionary但我也不想滥用垃圾收集器并使用太多内存。

我想过使用WeakReference字典,但不确定这是否是最好的方法。

你会推荐什么?如何正确处理可能多次调用的昂贵方法的结果?

更新:

我对 C# 2.0 库(单个 DLL,非可视)的建议感兴趣。该库可能用于 Web 应用程序的桌面。

更新 2:

这个问题也与只读对象有关。我将属性的值从 更改DictionaryReadOnlyDictionary

更新 3:

是相对简单的T类型(例如字符串)。这V是一个自定义类。您可能会假设V创建一个实例的成本很高。字典可能包含从 0 到几千个元素。

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

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

4

5 回答 5

3

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

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

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

如果您没有明确的策略何时应从缓存中删除对象,则缓存是内存泄漏的另一个说法。由于您正在尝试 WeakReference,我假设您不知道何时是清除缓存的最佳时机。

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

于 2012-06-10T20:25:53.830 回答
1

有四种主要机制可供您使用(懒惰是 4.0 中的,所以没有选择)

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

每个都有自己的优势。

我建议使用一个值持有者,它会在第一次调用持有者的 GetValue 方法时填充字典。那么您可以根据需要使用该值并且它只执行一次并且只在需要时完成。

欲了解更多信息,请参阅马丁福勒页面

于 2012-06-10T20:34:28.363 回答
1

您确定需要缓存整个字典吗?

根据您的说法,最好保留一个最近使用的键值对列表。

如果在列表中找到键,则返回值。

如果不是,则创建一个值(据说这比创建所有值更快,并且使用的内存也更少)并将其存储在列表中,从而删除未使用时间最长的键值对。

这是一个非常简单的 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 足够小,以便搜索保持快速,并避免过多的内存使用。每次使用项目时调用 Add。

于 2012-06-10T20:58:19.007 回答
1

WeakReference如果对象在缓存中存在的有用生命周期与对象的引用生命周期相当,则应用程序应用作缓存机制。例如,假设您有一个方法,该方法将ReadOnlyDictionary基于反序列化 a 来创建 a String。如果一个常见的使用模式是读取一个字符串,创建一个字典,用它做一些事情,放弃它,然后用另一个字符串重新开始,WeakReference可能并不理想。另一方面,如果您的目标是将许多字符串(其中不少是相等的)反序列化为ReadOnlyDictionary如果重复尝试反序列化相同的字符串产生相同的实例,这可能非常有用。请注意,节省不仅来自于只需构建一次实例的工作,而且还来自以下事实:(1)不必在内存中保留多个实例,以及(2)如果ReadOnlyDictionary变量引用同一个实例,则可以知道它们是等价的,而无需检查实例本身。相比之下,确定两个不同的ReadOnlyDictionary实例是否等价可能需要检查每个实例中的所有项目。必须进行许多此类比较的代码可以从使用WeakReference缓存中受益,这样保存等效实例的变量通常会保存相同的实例。

于 2012-06-11T20:54:33.507 回答
0

我认为您可以依靠两种机制进行缓存,而不是自己开发。第一个,正如您自己建议的那样,是使用 Wea​​kReference,并让垃圾收集器决定何时释放此内存。

你有第二种机制——内存分页。如果字典是一口气创建的,它可能会存储在堆的或多或少连续的部分中。只需保持字典处于活动状态,如果不需要,让 Windows 将其分页到交换文件。根据您的使用情况(您的字典访问的随机性),您最终可能会获得比 WeakReference 更好的性能。

如果您接近地址空间限制(这仅在 32 位进程中发生),则第二种方法是有问题的。

于 2012-06-10T20:25:29.447 回答