我需要在3D渲染器中实现LRU算法以进行纹理缓存。我在Linux上用C ++编写代码。
在我的情况下,我将使用纹理缓存来存储图像数据的“图块”(16x16像素块)。现在假设我在缓存中进行查找,获得命中(tile在缓存中)。如何将该条目的“缓存”内容返回给函数调用者?我解释。我想,当我在高速缓冲存储器中加载一个磁贴时,我分配内存来存储16x16像素,然后加载该磁贴的图像数据。现在有两种解决方案可以将缓存条目的内容传递给函数调用者:
1)作为指向图块数据的指针(快速,内存有效),
TileData *tileData = cache->lookup(tileId); // not safe?
2)或者我需要在函数调用者分配的内存空间中重新复制缓存中的tile数据(复制可能很慢)。
void Cache::lookup(int tileId, float *&tileData) { // find tile in cache, if not in cache load from disk add to cache, ... ... // now copy tile data, safe but ins't that slow? memcpy((char*)tileData, tileDataFromCache, sizeof(float) * 3 * 16 * 16); } float *tileData = new float[3 * 16 * 16]; // need to allocate the memory for that tile // get tile data from cache, requires a copy cache->lookup(tileId, tileData);
我会选择1)但问题是,如果在查找之后从缓存中删除了tile,并且该函数尝试使用返回指针访问数据会发生什么?我看到的唯一解决方案是使用一种引用计数(auto_ptr)的形式,其中数据实际上只有在不再使用时才被删除?
应用程序可能会访问多个纹理。我似乎无法找到一种创建每个纹理和纹理的每个图块独有的键的方法。例如,我可能在缓存中有来自file1的tile 1和来自file2的tile1,所以在tildId = 1上进行搜索是不够的......但我似乎无法找到一种方法来创建解释该文件的密钥name和tileID。我可以构建一个包含文件名和tileID(FILENAME_TILEID)的字符串,但是用作键的字符串不会比整数慢得多吗?
最后我对时间戳有疑问。许多论文建议使用时间戳来对缓存中的条目进行排序。使用时间戳有什么好功能? time()函数,clock()?有没有比使用时间戳更好的方法?
很抱歉,我发现这是一条非常长的消息,但LRU的实现并不像听起来那么简单。
答案 0 :(得分:4)
您的问题的答案:
1)返回一个shared_ptr(或逻辑上等同于它的东西)。然后,所有“什么时候安全删除这个对象”的问题都会消失。
2)我首先使用字符串作为键,看看它实际上是否太慢。如果字符串不是太长(例如你的文件名不是太长),那么你可能会发现它比你预期的要快。如果你确实发现字符串键不够有效,你可以尝试类似于为字符串计算哈希码并向其添加tile ID ...这可能会在实践中起作用,尽管总是有可能哈希冲突。但是你可以在启动时运行一个碰撞检查例程来生成所有可能的文件名+ tileID组合,并在映射到相同的键值时提醒你,这样至少你在测试期间会立即知道有没有问题,并可以做一些事情(例如通过调整您的文件名和/或您的哈希码算法)。这假定事先知道所有文件名和tile ID是什么。
3)我不建议使用时间戳,这是不必要和脆弱的。相反,尝试这样的事情(伪代码):
typedef shared_ptr<TileData *> TileDataPtr; // automatic memory management!
linked_list<TileDataPtr> linkedList;
hash_map<data_key_t, TileDataPtr> hashMap;
// This is the method the calling code would call to get its tile data for a given key
TileDataPtr GetData(data_key_t theKey)
{
if (hashMap.contains_key(theKey))
{
// The desired data is already in the cache, great! Just move it to the head
// of the LRU list (to reflect its popularity) and then return it.
TileDataPtr ret = hashMap.get(theKey);
linkedList.remove(ret); // move this item to the head
linkedList.push_front(ret); // of the linked list -- this is O(1)/fast
return ret;
}
else
{
// Oops, the requested object was not in our cache, load it from disk or whatever
TileDataPtr ret = LoadDataFromDisk(theKey);
linkedList.push_front(ret);
hashMap.put(theKey, ret);
// Don't let our cache get too large -- delete
// the least-recently-used item if necessary
if (linkedList.size() > MAX_LRU_CACHE_SIZE)
{
TileDataPtr dropMe = linkedList.tail();
hashMap.remove(dropMe->GetKey());
linkedList.remove(dropMe);
}
return ret;
}
}
答案 1 :(得分:0)
与您的问题顺序相同:
从性能角度来看,复制纹理日期似乎不合理。只要您可以安全地编码它,参考计数声音就会好得多。一旦渲染器未使用数据存储器,或就会释放数据存储器。
我假设你将使用某种hash table来查找你所描述的部分。您问题的常见解决方案有两个部分:
使用合并多个值的合适散列函数,例如:纹理文件名和图块ID。基本上,您创建一个被视为一个实体的复合键。散列函数可以是所有基本组件的哈希的XOR运算,或更复杂的东西。
选择合适的哈希函数对于性能原因至关重要 - 如果上述函数不够随机,则会有很多hash collisions。
使用合适的复合等式检查来处理哈希冲突的情况。
通过这种方式,您可以在单个哈希表查找中查找所有感兴趣属性的组合。
使用时间戳是不开始工作 - 期间。关于缓存的大多数来源通常在考虑网络资源缓存(例如HTTP缓存)时描述所讨论的算法。由于三个原因,这在这里不起作用:
使用自然时间只会意味着您打算实施将其考虑在内的缓存策略,例如: 10分钟后删除缓存条目。除非你正在做一些非常这样的奇怪的东西在3D渲染器中没有任何意义。
即使您使用高精度计时器,时间戳的实际分辨率也相对较低。大多数计时器源的精度大约为1毫秒,这对于处理器而言是非常长的时间 - 在此期间,您的渲染器将通过多个纹理条目进行处理。
你知道定时器通话的费用是多少吗?像这样滥用它们甚至可能使你的系统执行更糟而不是没有任何缓存......
此问题的常见解决方案是根本不使用计时器。 LRU算法只需要知道两件事:
允许的最大条目数。
现有条目的顺序w.r.t.他们的最后一次访问。
项目(1)来自系统的配置,通常取决于可用的存储空间。项(2)通常意味着使用组合链表/散列表数据结构,其中散列表部分提供快速访问,并且链表保留访问顺序。每次访问一个条目时,它都会被放置在列表的末尾,而旧的条目将从它的开头被删除。
使用组合数据结构而不是两个单独的数据结构允许从哈希表中删除条目,而无需进行查找操作。这样可以提高整体性能,但并非绝对必要。
答案 2 :(得分:0)
正如所承诺的那样,我发布了我的代码。如果我犯了错误或者我可以进一步改进,请告诉我。我现在要研究如何在多线程环境中工作。再次感谢Jeremy和Thkala的帮助(对不起,代码不符合评论栏)。
#include <cstdlib>
#include <cstdio>
#include <memory>
#include <list>
#include <unordered_map>
#include <cstdint>
#include <iostream>
typedef uint32_t data_key_t;
class TileData
{
public:
TileData(const data_key_t &key) : theKey(key) {}
data_key_t theKey;
~TileData() { std::cerr << "delete " << theKey << std::endl; }
};
typedef std::shared_ptr<TileData> TileDataPtr; // automatic memory management!
TileDataPtr loadDataFromDisk(const data_key_t &theKey)
{
return std::shared_ptr<TileData>(new TileData(theKey));
}
class CacheLRU
{
public:
// the linked list keeps track of the order in which the data was accessed
std::list<TileDataPtr> linkedList;
// the hash map (unordered_map is part of c++0x while hash_map isn't?) gives quick access to the data
std::unordered_map<data_key_t, TileDataPtr> hashMap;
CacheLRU() : cacheHit(0), cacheMiss(0) {}
TileDataPtr getData(data_key_t theKey)
{
std::unordered_map<data_key_t, TileDataPtr>::const_iterator iter = hashMap.find(theKey);
if (iter != hashMap.end()) {
TileDataPtr ret = iter->second;
linkedList.remove(ret);
linkedList.push_front(ret);
++cacheHit;
return ret;
}
else {
++cacheMiss;
TileDataPtr ret = loadDataFromDisk(theKey);
linkedList.push_front(ret);
hashMap.insert(std::make_pair<data_key_t, TileDataPtr>(theKey, ret));
if (linkedList.size() > MAX_LRU_CACHE_SIZE) {
const TileDataPtr dropMe = linkedList.back();
hashMap.erase(dropMe->theKey);
linkedList.remove(dropMe);
}
return ret;
}
}
static const uint32_t MAX_LRU_CACHE_SIZE = 8;
uint32_t cacheMiss, cacheHit;
};
int main(int argc, char **argv)
{
CacheLRU cache;
for (uint32_t i = 0; i < 238; ++i) {
int key = random() % 32;
TileDataPtr tileDataPtr = cache.getData(key);
}
std::cerr << "Cache hit: " << cache.cacheHit << ", cache miss: " << cache.cacheMiss << std::endl;
return 0;
}