我正在试图找出为资源执行缓存的最佳方法。我主要是寻找原生的C / C ++ / C ++ 11解决方案(即我没有提升和喜欢的选项)。
从缓存中检索时我正在做的事情是这样的:
Object *ResourceManager::object_named(const char *name) {
if (_object_cache.find(name) == _object_cache.end()) {
_object_cache[name] = new Object();
}
return _object_cache[name];
}
其中_object_cache
的定义类似于:std::unordered_map <std::string, Object *> _object_cache;
我想知道的是关于这样做的时间复杂性,是否找到了触发线性时间搜索,还是作为某种查找操作完成?
我的意思是如果我在给定的例子上做_object_cache["something"];
它将返回该对象,或者如果它不存在,它将调用默认构造函数插入一个不是我想要的对象。我发现这有点违反直觉,我希望它以某种方式报告(例如,返回nullptr
),value
无法检索到key
,而不是猜猜我想要的是什么。
但是,如果我在密钥上执行find
,它是否会触发一个大搜索,实际上它将以线性时间运行(因为找不到密钥会查看每个密钥)?< / p>
这是一个很好的方法吗,或者是否有人有一些建议,也许可以使用查找或某些东西来知道密钥是否可用,我可以经常访问,如果是这样的话花一些时间用于搜索我想消除它,或者至少尽可能快地进行搜索。
感谢您的任何意见。
答案 0 :(得分:4)
默认构造函数(由_object_cache["something"]
触发)是你想要的;指针类型的默认构造函数(例如Object *
)给出nullptr
(8.5p6b1,脚注103)。
所以:
auto &ptr = _object_cache[name];
if (!ptr) ptr = new Object;
return ptr;
使用无序地图(auto &ptr
)的引用作为本地变量,以便分配到地图并在同一操作中设置返回值。在C ++ 03中或者如果你想要显式,请写Object *&ptr
(对指针的引用)。
请注意,您应该使用unique_ptr
而不是原始指针来确保缓存管理所有权。
顺便说一下,find
与operator[]
的效果相同;平均常数,最坏情况线性(仅当无序映射中的每个键具有相同的散列时)。
答案 1 :(得分:3)
以下是我写这个的方法:
auto it = _object_cache.find(name);
return it != _object_cache.end()
? it->second
: _object_cache.emplace(name, new Object).first->second;
答案 2 :(得分:2)
std :: unordered_map上find
的复杂性是O(1)(常量),特别是std :: string键具有良好的散列,导致非常低的冲突率。即使方法的名称是find
,它也不会像你指出的那样进行线性扫描。
如果你想进行某种缓存,这个容器绝对是一个好的开始。
答案 3 :(得分:0)
请注意,cache
通常不仅是快速O(1)
访问权限,还包含有界数据结构。当添加越来越多的元素时,std::unordered_map
将动态增加其大小。当资源有限时(例如,将大量文件从磁盘读入内存),您需要一个有限且快速的数据结构来提高系统的响应能力。
相比之下,cache
只要size()
到达capacity()
,就会使用逐出策略,方法是替换价值最低的元素。
您可以在cache
之上实施std::unordered_map
。然后可以通过重新定义insert()
成员来实施驱逐策略。如果您想要N
- 方式(对于小而且固定的N
)关联缓存(即一个项目最多可以替换N
个其他项目),您可以使用{{ 1}}接口替换其中一个桶的条目。
对于完全关联的缓存(即任何项目可以替换任何其他项目),您可以通过添加bucket()
作为辅助数据结构来使用最近最少使用的驱逐策略:
std::list
通过将这两个结构包装在using key_tracker_type = std::list<K>;
using key_to_value_type = std::unordered_map<
K,std::pair<V,typename key_tracker_type::iterator>
>;
类中,您可以定义cache
以在容量已满时触发替换。发生这种情况时,您insert()
最近最少使用的项目和pop_front()
当前项目进入列表。
在Tim Day's blog there is an extensive example上使用完整源代码实现上述缓存数据结构。它的实现也可以使用Boost.Bimap或Boost.MultiIndex有效地完成。
答案 4 :(得分:0)
map / unordered_map的insert / emplace接口足以完成您想要的操作:找到位置,并在必要时插入。由于此处的映射值是指针,因此ekatmur的响应是理想的。如果你的值是地图中完全成熟的对象而不是指针,你可以使用这样的东西:
Object& ResourceManager::object_named(const char *name, const Object& initialValue) {
return _object_cache.emplace(name, initialValue).first->second;
}
如果没有与name
具有相同值的键,则值initialValue
和name
构成需要插入的键值对的参数。 emplace
返回一对,second
表示是否插入了任何内容(name
中的密钥是新密钥) - 我们在此不关心;并且first
是指向(可能是新创建的)键值对条目的迭代器,其键值等于name
的值。因此,如果密钥已经存在,则取消引用first
会为密钥提供原始Ojbect
,而该密钥尚未被initialValue
覆盖;否则,使用name
的值重新插入密钥,并从initialValue
复制条目的值部分,first
指向该值。
ekatmur的回答相当于:
Object& ResourceManager::object_named(const char *name) {
bool res;
auto iter = _object_cache.end();
std::tie(iter, res) = _object_cache.emplace(name, nullptr);
if (res) {
iter->second = new Object(); // we inserted a null pointer - now replace it
}
return iter->second;
}
但是由于operator[]
创建的默认构造指针值为null而无法确定是否需要分配新的Object
。它更简洁,更容易阅读。