【发布时间】:2015-06-15 17:12:17
【问题描述】:
TL;DR 我正在寻找一种从IComparer<T> 获取IEqualityComparer<T> 的方法,无论哪种数据类型是T,如果T 是string,则包括不区分大小写的选项。或者我需要一个不同的解决方案来解决这个问题。
这里是完整的故事:我正在使用 LFU 策略实现简单的通用缓存。要求是必须可以选择缓存是区分大小写还是不区分大小写——如果string 恰好是缓存键的数据类型(这不是必需的)。在我主要开发缓存的解决方案中,我预计会有数千亿次缓存查找,缓存大小最多为 100.000 个条目。由于这些数字,我立即放弃使用任何导致分配的字符串操作(例如.ToLower().GetHashCode() 等),而是选择使用IComparer 和IEqualityComparer,因为它们是标准的BCL 功能。此缓存的用户可以将比较器传递给构造函数。以下是相关代码片段:
public class LFUCache<TKey,TValue>
{
private readonly Dictionary<TKey,CacheItem> entries;
private readonly SortedSet<CacheItem> lfuList;
private class CacheItem
{
public TKey Key;
public TValue Value;
public int UseCount;
}
private class CacheItemComparer : IComparer<CacheItem>
{
private readonly IComparer<TKey> cacheKeyComparer;
public CacheItemComparer(IComparer<TKey> cacheKeyComparer)
{
this.cacheKeyComparer = cacheKeyComparer;
if (cacheKeyComparer == null)
this.cacheKeyComparer = Comparer<TKey>.Default;
}
public int Compare(CacheItem x, CacheItem y)
{
int UseCount = x.UseCount - y.UseCount;
if (UseCount != 0) return UseCount;
return cacheKeyComparer.Compare(x.Key, y.Key);
}
}
public LFUCache(int capacity, IEqualityComparer<TKey> keyEqualityComparer,
IComparer<TKey> keyComparer) // <- here's my problem
{
// ...
entries = new Dictionary<TKey, CacheItem>(keyEqualityComparer);
lfuList = new SortedSet<CacheItem>(new CacheItemComparer(keyComparer));
}
// ...
}
keyEqualityComparer 用于管理缓存条目(例如,如果用户愿意,键“ABC”和“abc”是相等的)。 keyComparer 用于管理按UseCount 排序的缓存条目,以便轻松选择最不常用的条目(在CacheItemComparer 类中实现)。
自定义比较的正确用法示例:
var cache = new LFUCache<string, int>(10000,
StringComparer.InvariantCultureIgnoreCase,
StringComparer.InvariantCultureIgnoreCase);
(这看起来很愚蠢,但StringComparer 实现了IComparer<string> 和IEqualityComparer<string>。)问题是如果用户给出不兼容的比较器(即不区分大小写的keyEqualityComparer 和区分大小写的keyComparer),那么最可能的结果是无效的 LFU 统计信息,因此充其量只能降低缓存命中率。另一种情况也不尽如人意。此外,如果密钥更复杂(我会有类似 Tuple<string,DateTime,DateTime> 的东西),可能会更严重地搞砸。
这就是为什么我希望在构造函数中只有一个比较器参数,但这似乎不起作用。我可以在IComparer<T>.Compare() 的帮助下创建IEqualityComparer<T>.Equals(),但我被困在IEqualityComparer<T>.GetHashCode() ——正如你所知,这非常重要。如果我可以访问比较器的私有属性来检查它是否区分大小写,我会使用CompareInfo 来获取哈希码。
我喜欢这种具有 2 种不同数据结构的方法,因为它为我提供了可接受的性能和可控的内存消耗——在我的笔记本电脑上大约 500.000 个缓存添加/秒,缓存大小为 10.000 个元素。 Dictionary<TKey,TValue>只是用于在O(1)中查找数据,SortedSet<CacheItem>在O(log n)中插入数据,通过在O(log n)中调用lfuList.Min找到要删除的元素,并找到要递增的条目在 O(log n) 中也使用计数。
欢迎任何有关如何解决此问题的建议。我会很感激任何想法,包括不同的设计。
【问题讨论】:
-
一种可能性是使用通用约束来定义一个静态工厂方法,该方法采用一个实现
IEqualityComparer<T>和IComparer<T>的比较器参数。那么至少你没有将同一个对象传递给两个不同的参数。 -
这听起来很有趣,但不知何故我无法理解代码应该是什么样子。你能分享几行粗略的代码吗? ;-)
-
当然。看我的回答。
-
我认为值得检查一下是否可以自己实现
SortedSet。如果您增加UseCount,则该组中只能向上更改一个位置。也许有一个 O(1) 的触摸实现。 Insert 应该是 O(1),因为它被添加到列表的末尾(也许 SortedSet 会这样做)。我将尝试一个新的示例实现。 -
有一个看起来很有趣 - dhruvbird.com/lfu.pdf
标签: c# iequalitycomparer icomparer memorycache