Рубрики
Без рубрики

Как построить кэш LRU менее чем за 100 строк кода

Примечание. Весь код в этой статье можно найти на GitHub. Когда я брал интервью у моего первого мягкого … Tagged с помощью компьютерной науки, питона, алгоритмов, учебного пособия.

Примечание. Весь код в этой статье можно найти на GitHub .

Когда я брал интервью у своей первой работы по разработке программного обеспечения, меня несколько раз использовали кэширование (LRU). Меня попросили как кодировать их и описать, как они используются в более крупных системах. И если вы провели справедливую долю интервью по кодированию, вероятно, у вас тоже есть.

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

Однако для кандидатов на работу это может быть резким опытом, особенно если вы никогда не использовали их в профессиональной обстановке. Это было так для меня. В первый раз, когда меня спросили о CACHES LRU, мой разум стал пустым. Я слышал о них, конечно. Но во всем моем изучении бинарных деревьев и кучей я не удосужился узнать, что в них входит. И жить перед интервьюером – это не идеальная обстановка, чтобы попытаться решить это.

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

Итак, давайте начнем – время тикает.

Какой кэш в последнее время используется (LRU)?

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

ОЗУ на вашем компьютере является примером кеша. Операционная система хранит данные в ОЗУ для более быстрого доступа, чем жесткий диск, и использует адрес ячейки памяти в качестве ключа.

Кэши операционной системы (кредит изображения: https://www.sciencedirect.com )

Кэши LRU – это конкретный тип кэша с уникальной функцией. Когда кэш LRU заканчивается пространством и необходимо удалить данные, он выяснит пару клавишных значений, которая была менее недавно извлечена из кэша.

Популярные примеры с открытым исходным кодом CACHES – это Redis и Memcached Анкет

Почему кэши LRU полезны?

Для технических предприятий, которые предоставляют API или пользовательский интерфейс, производительность и доступность имеют решающее значение. Подумайте о том, в последний раз, когда вы посещали медленный сайт. Вы остались и ждали его загрузки, или вы ушли и посетили другой сайт? Скорее всего, последний. Медленные или недостаточно эффективные веб-сайты могут привести к миллионам потерянного дохода.

К сожалению, многие из тех же систем, которые полагаются на высокое время безотказной работы, также должны хранить горы данных. Это часто относится к сайтам социальных сетей и электронной коммерции. Эти сайты хранят свои данные в какой -то базе данных, будь то SQL или NOSQL. Хотя это стандартная практика, проблема возникает, когда вам нужно получить эти данные. Запрос баз данных, особенно когда они содержат много данных, может быть довольно медленным.

Введите кэш.

Поскольку кэши держат данные в памяти, они гораздо более эффективны, чем традиционные базы данных. А для сайтов социальных сетей, где 20% наиболее популярного контента приводит к 80% трафика, кэширование может значительно уменьшить нагрузку на базы данных.

Сравнение скорости SQL и кэша (кредит на изображение: https://dzone.com/articles/redis-vs-mysql-benchmarks )

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

Однако знать, как работают кэши и как их использовать, легко. Знать, как построить его самостоятельно, – это сложная часть. И это именно то, на чем мы сосредоточимся.

Как построить кэш LRU

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

Требования

Прежде чем мы начнем создавать кеш, мы должны понять, каковы требования. Первым будет API. Какие методы нам нужно реализовать?

В то время как кэши качества производства богаты функциями, мы будем простыми. Вам нужно только создать два метода:

  • получить (ключ)

  • SET (ключ, значение)

Но это еще не все. Сам кэш LRU имеет ряд требований:

  1. Когда максимальный размер кэша достигнут, удалите наименее недавно используемый ключ.

  2. Всякий раз, когда ключ извлекается или обновляется, он становится последним использованным.

  3. Оба операции GET и SET должны быть завершены в O (1) Сложность времени (Это означает, что независимо от того, насколько велик кеш, для его завершения требуется столько же времени).

  4. При получении ключа, который не существует, верните нулевое значение.

Имея в виду эти требования, мы можем приступить к работе.

фото ConvertKit на Неспособный

Структура данных

Первый вопрос, на который нам нужно ответить, – какая структура данных должна поддержать наш кэш LRU? В конце концов, кэш не является самой структурой данных.

Поскольку кэш использует Get и Set, и необходимо работать в O (1) время, вы, возможно, подумали о хеш -карте или словаре. Это действительно выполнит некоторые требования. Но как насчет удаления ключа LRU? Словарь не позволяет нам знать, какой старый ключ.

Мы могли бы использовать штамп во времени как часть словарного значения и обновить ее всякий раз, когда ключ извлечен. Это скажет нам, какая пара ключевых значений самая старая. Проблема в том, что нам нужно будет пройти через все записи словаря, чтобы проверить, что является самым старым – нарушая наше требование O (1).

Так в чем же ответ?

Здесь я должен позволить вам в секрете. Нам на самом деле понадобится два Структуры данных: одна для получения значений (Dictionary/Hash Map) и одна для сортировки элементов по частоте использования.

Вторая структура данных

Так что же должна быть вторая структура данных? Если вы думали о массиве, вы приближаетесь.

Мы можем использовать массив, чтобы сортировать элементы. И каждый ключ в словаре может ссылаться на индекс значения в массиве. Всякий раз, когда выбирается ключ, мы можем переместить это значение на переднюю часть массива, отбросив остальные элементы обратно и обновить индекс в словаре.

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

Изображение предоставлено: Автор

Но мы близки. Нам просто нужна структура данных с теми же преимуществами сортировки, что и массив, который также может получить, устанавливать и удалять данные в O (1). И структура данных, которая соответствует всем этим требованиям, является Двойной связанный список (DLL) Анкет

Вдвойне связанный список такой же, как и Связанный список , кроме того, каждый узел в списке имеет дополнительную ссылку на предыдущий узел, а также следующий узел.

Изображение предоставлено: https://medium.com/flawless-app-stories/doubly-linked-lists-swift-4-ae3cf8a5b975

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

Изображение предоставлено: https://corvostore.github.io/#LRU

Создание кеша LRU

Теперь, когда мы знаем состав структуры данных нашего кэша LRU, мы можем начать его строить!

class LRUCache(object):
    def __init__(self, max_size=10):
        if max_size <= 0:
            raise Exception('Max size must be larger than zero')
        self.max_size = max_size
        self.list = DoublyLinkedList()
        self.nodes = {}

    def set(self, key, value):
        node = self.nodes.get(key, None)

        #if the key already exists, update the value
        if node != None:
            node.data.value = value
            self.list.move_front(node)
            return

        # if the cache has reached its max size, remove the least recently used key
        if self.list.capacity == self.max_size:
            expired = self.list.remove_tail()
            del self.nodes[expired.data.key]

        self.nodes[key] = self.list.unshift(KVPair(key, value))

    def get(self, key):
        node = self.nodes.get(key, None)

        # if the key doesn't exist, return None
        if node is None:
            return None

        # make the key to the front of linked list (most recently used)
        self.list.move_front(node)
        return node.data.value

Глядя на этот код, вы можете предположить, что мы закончили. Но есть улов. Вы заметили двойного списка в строке 6? Python не поставляется с двойным списком в стандартной библиотеке.

На самом деле, большинство языков программирования не оснащены DLL. И поскольку проблемы с живым кодированием редко позволяют вам использовать сторонние библиотеки, нам придется реализовать их сами.

Создание вдвойне связанного списка

Когда дело доходит до создания DLL, наиболее важным аспектом является дизайн. Существует много различных типов связанных списков с различными компромиссами, но для наших случаев использования мы будем делать круговой DLL. Это даст нам быстрый доступ к голове и хвосту, что важно для кэша LRU.

Круглый дважды связанный список с корневым узлом (изображение Кредит: Автор)

class Node(object):
    def __init__(self, data):
        self.data = data
        self.next = None
        self.prev = None

class DoublyLinkedList(object):
    def __init__(self):
        self.root = Node(None)
        self.capacity = 0

        # the root node should point to itself when the list is empty
        self.root.next = self.root
        self.root.prev = self.root

В то время как связанные списки обычно поставляются со многими удобными методами, нам нужно только реализовать небольшой подмножество:

  1. MOVE_FRONT (Узел)

  2. без устремления (значение)

  3. Удалить_ хвост

Движение

Всякий раз, когда ключ извлекается из нашего кеша, нам нужно сделать этот ключ последним использованным. Это включает в себя перемещение этого ключа к передней части DLL.

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

Переместите переднюю анимацию (изображение Кредит: Автор)

def move_front(self, node):
    # guard against empty nodes
    if node is None:
        return None

    # Step 1: remove the node from its current position
    node.next.prev = node.prev
    node.prev.next = node.next

    # Step 2: change the node so it points to the root and the old head node
    node.prev = self.root
    node.next = self.root.next # old head node

    # Step 3: Update the root node so it points to the new head
    self.root.next.prev = node # update old head prev pointer
    self.root.next = node
    return node

Непрерывно

Непрерывный метод для DLL работает очень аналогично массиву, где мы вставляем значение спереди или головке списка. Наш кэш LRU будет использовать этот метод всякий раз, когда называется (ключ, значение).

К счастью для нас, без устремления разделяет много логики с Move_front. Если мы немного обновим ove_front, мы сможем использовать его для реализации без устранения.

Непрерывная анимация (изображение кредитование: https://visualgo.net )

def unshift(self, data):
    node = Node(data)
    self.move_front(node)
    self.capacity += 1
    return node

def move_front(self, node):
    if node is None:
        return None
    elif node.prev is not None and node.next is not None:
        # if node is already in the list, remove it from its current position
        node.next.prev = node.prev
        node.prev.next = node.next

    node.prev = self.root
    node.next = self.root.next

    self.root.next.prev = node
    self.root.next = node
    return node

Удалить

Если наш кэш достигнет своего максимального размера, нам нужно будет удалить наименее недавно используемый ключ. Это включает в себя удаление хвоста нашего DLL. Подобно непрерывному, Remove_tail разделяет некоторую общую логику с MOVE_FRONT. В то время как мы не можем повторно использовать MOVE_FRONT для remove_tail, мы можем абстрагировать некоторую общую логику.

Удалить анимацию хвоста (изображение кредитование: https://visualgo.net )

def remove_tail(self):
    # if the list is already empty, bail out
    if self.capacity == 0:
        return None

    removed = self.isolate(self.root.prev)
    self.capacity -= 1
    return removed

@staticmethod
# isolate removes a node from its current position
# it also sets its next and prev pointers to None to prevent memory leaks
def isolate(node):
    node.next.prev = node.prev
    node.prev.next = node.next
    node.next = None
    node.prev = None
    return node

Сделать все это вместе

Когда мы объединяем наш кэш LRU и DLL, это то, что мы получаем:

class KVPair(object):
    def __init__(self, k, v):
        self.key = k
        self.value = v

class Node(object):
    def __init__(self, data):
        self.data = data
        self.next = None
        self.prev = None

class DoublyLinkedList(object):
    def __init__(self):
        self.root = Node(None)
        self.capacity = 0

        self.root.next = self.root
        self.root.prev = self.root

    def unshift(self, data):
        node = Node(data)
        self.move_front(node)
        self.capacity += 1
        return node

    def move_front(self, node):
        if node is None:
            return None
        elif node.prev is not None and node.next is not None:
            self.isolate(node)

        node.prev = self.root
        node.next = self.root.next
        self.root.next.prev = node
        self.root.next = node
        return node

    def remove_tail(self):
        if self.capacity == 0:
            return None

        removed = self.isolate(self.root.prev)
        self.capacity -= 1
        return removed

    @staticmethod
    def isolate(node):
        node.next.prev = node.prev
        node.prev.next = node.next
        node.next = None
        node.prev = None
        return node

class LRUCache(object):
    def __init__(self, max_size=10):
        if max_size <= 0:
            raise Exception('Max size must be larger than zero')
        self.max_size = max_size
        self.list = DoublyLinkedList()
        self.nodes = {}

    def set(self, key, value):
        node = self.nodes.get(key, None)
        if node != None:
            node.data.value = value
            self.list.move_front(node)
            return

        if self.list.capacity == self.max_size:
            expired = self.list.remove_tail()
            del self.nodes[expired.data.key]

        self.nodes[key] = self.list.unshift(KVPair(key, value))

    def get(self, key):
        node = self.nodes.get(key, None)
        if node is None:
            return None

        self.list.move_front(node)
        return node.data.value

И так же, как это, через 10 минут мы создали базовый кэш LRU менее чем в 100 строках кода. Для дополнительной практики вы можете попытаться добавить возможность удалить ключ, создать простые модульные тесты, чтобы доказать, что он работает, или создать его на другом языке.

Как я уже упоминал ранее, если Python – это не ваше дело, не волнуйтесь. У меня есть больше примеров для JavaScript, Ruby и Go Анкет Я также открыт, чтобы получить запросы на другие языки.

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

Оригинал: “https://dev.to/sunnyb/how-to-build-an-lru-cache-in-less-than-10-minutes-and-100-lines-of-code-5hmn”