Лучшее понимание алгоритма LRU

Мне нужно реализовать алгоритм LRU в 3D-рендерере для кеширования текстур. Я пишу код на C ++ под Linux.

  • В моем случае я буду использовать кеширование текстур для хранения «тайлов» данных изображения (блок 16×16 пикселей). Теперь представьте, что я делаю поиск в кеше, получаю попадание (тайл в кеше). Как вернуть содержимое «кэша» для этой записи в функцию-вызывающую функцию? Я объясняю. Я представляю, что когда я загружаю плитку в кэш-память, я выделяю память для хранения, например, 16×16 пикселей, а затем загружаю данные изображения для этой плитки. Теперь есть два решения для передачи содержимого записи в кэш вызывающей функции:
    1) либо как указатель на данные тайла (быстро, эффективно по памяти),

    TileData * tileData = cache-> lookup (tileId); // не безопасно?

    2) или мне нужно скопировать данные тайла из кэша в пространство памяти, выделенное вызывающей функцией (копирование может быть медленным).

    void Cache :: lookup (int tileId, float * tileData)
    {
    // найти плитку в кеше, если не в кеше загрузить с диска добавить в кеш, ...
    ...
    // теперь копируем данные тайла, безопасно, но не так ли медленно?
    memcpy ((char *) tileData, tileDataFromCache, sizeof (float) * 3 * 16 * 16);
    }
    float * tileData = new float [3 * 16 * 16]; // нужно выделить память для этой плитки
    // получить данные тайла из кеша, требуется копия
    cache-> lookup (tileId, tileData);
    

    Я бы пошел с 1), но проблема в том, что произойдет, если плитка будет удалена из кэша сразу после поиска, и что функция пытается получить доступ к данным с помощью указателя возврата? Единственное решение, которое я вижу в этом, состоит в том, чтобы использовать форму подсчета ссылок (auto_ptr), когда данные фактически удаляются только тогда, когда они больше не используются?

  • приложение может получить доступ к более чем 1 текстуре. Я не могу найти способ создать ключ, который уникален для каждой текстуры и каждой плитки текстуры. Например, у меня могут быть плитки 1 из файла file1 и tile1 из файла file2 в кэше, поэтому недостаточно выполнить поиск по tildId = 1 … но я не могу найти способ создания ключа, который отвечает за файл имя и tileID. Я могу построить строку, которая будет содержать имя файла и tileID (FILENAME_TILEID), но не будет ли строка, используемая в качестве ключа, намного медленнее, чем целое число?

  • Наконец, у меня есть вопрос относительно отметки времени. Многие статьи предлагают использовать метку времени для упорядочения записи в кеше. Что такое хорошая функция для использования метки времени? функция времени (), часы ()? Есть ли лучший способ, чем использовать метки времени?

Извините, я понимаю, что это очень длинное сообщение, но LRU не так просто реализовать, как кажется.

2

Решение

Ответы на ваши вопросы:

1) Вернуть shared_ptr (или что-то логически эквивалентное ему). Тогда все проблемы «когда это безопасно удалить этот объект» в значительной степени исчезнут.

2) Я бы начал с использования строки в качестве ключа и посмотрел, слишком ли она медленная или нет. Если строки не слишком длинные (например, ваши имена файлов не слишком длинные), вы можете обнаружить, что это быстрее, чем вы ожидаете. Если вы обнаружите, что строковые ключи недостаточно эффективны, вы можете попробовать что-то вроде вычисления хеш-кода для строки и добавления к ней идентификатора тайла … это, вероятно, сработает на практике, хотя всегда будет возможность хеш-столкновение. Но вы можете запустить подпрограмму проверки столкновений при запуске, которая сгенерирует все возможные комбинации имя файла + tileID и предупредит вас, если отобразится одно и то же значение ключа, так что, по крайней мере, вы сразу узнаете во время тестирования, когда есть проблема и может что-то с этим сделать (например, путем корректировки ваших имен файлов и / или ваш алгоритм хэш-кода). Это предполагает, что все имена файлов и идентификаторы плиток будут известны заранее.

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;
}
}
4

Другие решения

В том же порядке, что и ваши вопросы:

  • Копирование даты текстуры не представляется разумным с точки зрения производительности. Подсчет ссылок звучит намного лучше, если вы действительно можете безопасно его кодировать. Память данных будет освобождена, как только она не будет использована средством визуализации. или же иметь ссылку, хранящуюся в кэше.

  • Я предполагаю, что вы собираетесь использовать какой-то хеш-таблица для поисковой части того, что вы описываете. Общее решение вашей проблемы состоит из двух частей:

    • Использование подходящей функции хеширования, которая объединяет несколько значений, например имя файла текстуры и идентификатор плитки. По сути вы создаете составной ключ это рассматривается как одно целое. Функция хеширования может быть операцией XOR хэшей всех элементарных компонентов или чем-то более сложным.

      Выбор подходящей хеш-функции имеет решающее значение с точки зрения производительности — если указанная функция не является достаточно случайной, у вас будет много хэш столкновения.

    • Использование подходящей составной проверки равенства для обработки случая хеш-столкновений.

    Таким образом, вы можете посмотреть сочетание всех атрибутов, представляющих интерес в одной хэш-таблицы.

  • Использование временных меток для этого не иду на работу — период. В большинстве источников, касающихся кеширования, обычно описываются рассматриваемые алгоритмы с учетом кеширования сетевых ресурсов (например, кеширования HTTP). Это не сработает здесь по трем причинам:

    1. Использование естественного времени имеет смысл только в том случае, если вы намерены внедрять политики кэширования, которые учитывают его, например, сброс записи в кеш через 10 минут. Если вы не делаете что-то очень странное что-то подобное не имеет смысла в 3D-рендерере.

    2. Метки времени имеют относительно низкое фактическое разрешение, даже если вы используете высокоточные таймеры. Большинство источников таймера имеют точность около 1 мс, что является очень долгое время для процессора — за это время ваш рендерер проработал бы несколько текстурных записей.

    3. Ты хоть представляешь, сколько стоят таймерные звонки? Такое злоупотребление ими может даже заставить вашу систему работать хуже чем не иметь кеша вообще …

    Обычное решение этой проблемы — вообще не использовать таймер. Алгоритм LRU должен знать только две вещи:

    1. Максимально допустимое количество записей.

    2. Порядок существующих записей w.r.t. их последний доступ.

    Элемент (1) исходит из конфигурации системы и обычно зависит от доступного пространства для хранения. Пункт (2) обычно подразумевает использование комбинированной структуры данных связанного списка / хеш-таблицы, где часть хеш-таблицы обеспечивает быстрый доступ, а связанный список сохраняет порядок доступа. Каждый раз, когда к записи обращаются, она помещается в конец списка, а старые записи удаляются с начала.

    Использование комбинированной структуры данных, а не двух отдельных, позволяет удалять записи из хеш-таблицы без необходимости выполнять операцию поиска. Это улучшает общую производительность, но это не является абсолютно необходимым.

0

Как и обещал, я публикую свой код. Пожалуйста, дайте мне знать, если я допустил ошибки или я мог бы улучшить это. Сейчас я собираюсь посмотреть, как это работает в многопоточной среде. Еще раз спасибо Джереми и Ткале за помощь (извините, код не подходит для блока комментариев).

#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;
}
0
По вопросам рекламы [email protected]