我们有一个C ++应用程序,我们尝试提高性能。我们发现数据检索需要花费大量时间,并且想要缓存数据。我们无法将所有数据存储在内存中,因为它很大。我们希望在内存中存储多达1000个项目。可以使用long
键对此项目编制索引。但是,当缓存大小超过1000时,我们想要删除最长时间未访问的项目,因为我们假设某种“引用位置”,即我们假设最近访问的缓存中的项目可能会再次访问。
你能建议一种实施方法吗?
我的初始实现是使用map<long, CacheEntry>
来存储缓存,并将accessStamp
成员添加到CacheEntry
,只要创建或访问条目,就会将其设置为递增计数器。当缓存已满并需要新条目时,代码将扫描整个缓存映射并找到具有最低accessStamp
的条目,并将其删除。
这样做的问题是,一旦缓存已满,每次插入都需要对缓存进行全面扫描。
另一个想法是除了缓存映射之外还要保留CacheEntries
的列表,并且在每次访问时将访问的条目移动到列表的顶部,但问题是如何快速找到该条目列表。
你能建议一个更好的方法吗?
感谢
splintor
答案 0 :(得分:15)
拥有map<long,CacheEntry>
,但不要在CacheEntry
中设置访问时间戳,而是在其他CacheEntry
个对象中添加两个链接,以使条目形成双向链接列表。每当访问一个条目时,将其移动到列表的头部(这是一个恒定时间操作)。通过这种方式,您可以轻松找到缓存条目,因为它可以从地图中访问,并且能够删除最近最少使用的条目,因为它位于列表的末尾(我的首选是将双向链表放在圆形,所以指向头部的指针也足以快速访问尾部。还要记得将您在地图中使用的密钥放入CacheEntry
,以便在从缓存中逐出时从地图中删除该条目。
答案 1 :(得分:11)
扫描1000个元素的地图将花费很少的时间,并且仅当项目不在缓存中时才执行扫描,如果您的参考想法的位置正确,则应该是一小部分时间。当然,如果你的想法是错误的,那么缓存可能是浪费时间。
答案 2 :(得分:2)
更新:我现在明白了......
这应该相当快。警告,前面有一些伪代码。
// accesses contains a list of id's. The latest used id is in front(),
// the oldest id is in back().
std::vector<id> accesses;
std::map<id, CachedItem*> cache;
CachedItem* get(long id) {
if (cache.has_key(id)) {
// In cache.
// Move id to front of accesses.
std::vector<id>::iterator pos = find(accesses.begin(), accesses.end(), id);
if (pos != accesses.begin()) {
accesses.erase(pos);
accesses.insert(0, id);
}
return cache[id];
}
// Not in cache, fetch and add it.
CachedItem* item = noncached_fetch(id);
accesses.insert(0, id);
cache[id] = item;
if (accesses.size() > 1000)
{
// Remove dead item.
std::vector<id>::iterator back_it = accesses.back();
cache.erase(*back_it);
accesses.pop_back();
}
return item;
}
插入和擦除可能有点贵,但考虑到局部性(很少缓存未命中)也可能不会太糟糕。无论如何,如果它们成为一个大问题,可以改为std :: list。
答案 3 :(得分:2)
可能使元素的“老化”更容易但以降低搜索性能为代价的另一种实现方法是将CacheEntry元素保存在std :: list中(或使用std::pair<long, CacheEntry>
。最新的元素被添加到列表的前面,因此它们随着它们的老化而“迁移”到列表的末尾。当你检查一个元素是否已经存在于缓存中时,你会扫描列表(这无疑是一个O(n)操作而不是在地图中作为O(log n)操作。)如果找到它,则将其从当前位置移除并重新插入列表的开头。如果列表长度超过1000个元素,从列表末尾删除所需数量的元素,将其修剪回1000个元素以下。
答案 4 :(得分:2)
在我的方法中,需要有一个快速查找存储对象的哈希表和一个用于维护上次使用序列的链表。
请求对象时。 1)尝试从哈希表中查找对象 2.yes)如果找到(该值具有链表中对象的指针),则将链表中的对象移动到链表的顶部。 2.否)如果没有,从链表中删除最后一个对象,也从哈希表中删除数据,然后将对象放入哈希表和链表顶部。
例如 假设我们只有3个对象的缓存。
请求序列是1 3 2 1 4。
1)哈希表:[1] 链表:[1]
2)哈希表:[1,3] 链表:[3,1]
3)哈希表:[1,2,3] 链表:[2,3,1]
4)哈希表:[1,2,3] 链表:[1,2,3]
5)哈希表:[1,2,4] 链表:[4,1,2] =&gt; 3出
答案 5 :(得分:1)
创建一个std:priority_queue&lt; map&lt; int,CacheEntry&gt; :: iterator&gt;,带有访问标记的比较器。对于插入,首先弹出队列中的最后一项,然后从地图中删除它。然后将新项插入到地图中,最后将它的迭代器推入队列。
答案 6 :(得分:0)
作为一种更简单的替代方案,您可以创建一个无限增长的地图,每10分钟左右清除一次(调整预期流量的时间)。
您也可以通过这种方式记录一些非常有趣的统计数据。
答案 7 :(得分:0)
我同意Neil,扫描1000个元素根本不需要时间。
但是如果你想要这样做,你可以使用你建议的附加列表,为了避免每次扫描整个列表,而不是只在地图中存储CacheEntry,你可以存储CacheEntry和指向列表中与此条目对应的元素的指针。
答案 8 :(得分:0)
我相信这是treaps的好候选人。优先级是时间(虚拟或其他),按升序排列(根目录较旧)和long
作为密钥。
还有second chance algorithm,这对缓存很有用。虽然你失去了搜索能力,但如果你只有1000个项目,那将不会产生很大的影响。
天真的方法是将一个与优先级队列相关联的地图包装在一个类中。您可以使用地图进行搜索并删除要删除的队列(首先从队列中删除,抓取该项目,然后从地图中按键删除)。
答案 9 :(得分:0)
另一种选择可能是使用boost::multi_index。它旨在将索引与数据分开,并允许在同一数据上使用多个索引。
我不确定扫描1000件物品确实会更快。它可能会使用更多的内存然后更好。或者减慢搜索速度和/或插入/删除。