Одним из методов ускорения работы приложений может быть внедрение кэширования.
Кэширование не только ускоряет время отклика для пользователей, но и снижает потребление ресурсов системы, делая ее эффективной.
Но что такое кэширование?
Говоря простым языком, кэширование означает хранение часто востребованных вещей ближе к тем, кто их запрашивает.
Давайте рассмотрим простую аналогию, очень красиво показанную в этом видео на youtube о кэшировании.
Предположим, вы хотите написать статью на определенную тему и хотите получить доступ к книгам из библиотеки.
Теперь вы можете ходить в библиотеку каждый раз, когда хотите посмотреть какую-то информацию из книг, но это будет очень дорогостоящая операция. Путешествие из дома в библиотеку, поиск книги, запись содержания, возвращение домой и повторение всего этого процесса снова и снова.
Однако вы можете взять эти книги в библиотеке и взять их с собой домой.
Теперь, когда вам понадобится найти какую-то информацию в книгах, они будут у вас дома, на расстоянии вытянутой руки.
В этой аналогии библиотека выступает в роли жесткого диска, на котором хранится много содержимого (книг), но частое обращение к нему обходится довольно дорого. А ваш дом действует как кэш, где хранятся только те книги, которые вам часто нужно искать, и доступ к которым относительно дешев.
Python поставляется со встроенным модулем functools, который содержит функции высшего порядка, которые либо принимают другие функции в качестве аргументов, либо возвращают другие функции.
Модуль functools также содержит некоторые функции высшего порядка, такие как lru_cache для кэширования, которые мы рассмотрим позже в статье.
А пока давайте попробуем самостоятельно реализовать простой кэш в python!
Сохраните приведенный ниже код в файле .py и попробуйте выполнить его. Давайте посмотрим, какой результат мы получим.
import requests
cache = dict()
def get_article_from_server(url):
print("Fetching article from server...")
response = requests.get(url)
return response.text
def get_article(url):
print("Getting article...")
if url not in cache:
cache[url] = get_article_from_server(url)
return cache[url]
get_article("https://realpython.com/sorting-algorithms-python/")
get_article("https://realpython.com/sorting-algorithms-python/")
get_article("https://realpython.com/sorting-algorithms-python/")
get_article("https://realpython.com/sorting-algorithms-python/")
Когда мы выполним приведенный выше код, мы получим следующий результат.
Посмотрите как,
Получение статьи с сервера. . .
выводится только один раз, и это первый раз?
Это потому, что после получения статьи мы сохраняем ее в кэше, который мы создали перед тем, как отправить ее в качестве ответа в функции get_article.
Следовательно, при последующих обращениях к той же статье нам не нужно извлекать ее с сервера/интернета, мы можем просто прочитать содержимое, сохраненное ранее в кэше, и вернуть результат.
Но мы не можем просто создавать кэш с нуля каждый раз, когда нам нужно его использовать. Кэш, который мы реализовали выше, был относительно простым, но что если нам нужны более сложные механизмы кэширования? К счастью, в python есть несколько встроенных функций/декораторов, которые помогут нам сделать именно это!
Давайте рассмотрим проблему вычисления факториала числа.
Ниже приведен простой код для вычисления факториала.
def factorial(n):
return n * factorial(n-1) if n else 1
Но проблема этого кода в том, что для каждого нового значения n он будет заново вычислять значения.
Я имею в виду, что если я вычисляю факториал числа 7, то на выходе получаю 5040, которое было вычислено 7x6x5x4x3x2x1.
Но теперь, если я вычислю факториал 5, он повторно вычислит 5x4x3x2x1, который он уже вычислил ранее при вычислении факториала 7.
Эти вычисления могут быть сохранены в кэше и получены без необходимости повторного вычисления, что экономит вычислительные ресурсы и время, делая систему в целом эффективной!
Давайте теперь изменим код, чтобы использовать кэш вместо этого.
from functools import cache
@cache
def factorial(n):
return n * factorial(n-1) if n else 1
Если мы вызовем factorial(10), то функция сделает 11 рекурсивных вызовов, так как кэш пуст. Но если после этого мы вызовем factorial(5), то код вообще не будет производить никаких вычислений, он просто найдет значение в кэше и вернет его.
А если мы вызовем factorial(12), то код сделает только два рекурсивных вызова для 11 и 12, поскольку значения до 10 уже были сохранены в кэше.
Отлично! Теперь мы знаем, как создать кэш и применить его к функции, выполняющей некоторые дорогостоящие операции, чтобы ускорить наш код и сделать его более эффективным с вычислительной точки зрения.
Давайте немного отвлечемся и вернемся к примеру с библиотекой…
Вы продолжали изучать темы одну за другой и в результате купили домой больше книг, чем могли бы оставить!
С точки зрения кэширования, здесь произошло то, что ваш кэш вырос до бесконечности.
Цель кэша — быть небольшим и хранить только некоторые данные, к которым часто обращаются пользователи. Если ваш кэш вырастет до размеров жесткого диска, это не будет иметь смысла, так как обслуживание этого кэша будет стоить очень дорого.
Итак, сейчас нам нужен механизм или политика, которая удалит из кэша содержимое/информацию, потерявшую актуальность и к которой больше не обращаются так часто. То есть нам нужна какая-то политика, которая помогла бы нам выгрузить часть книг, хранящихся в нашем доме, обратно в библиотеку. Именно здесь на помощь приходит политика вытеснения кэша.
Политики вытеснения кэша
Существует множество стратегий, которые позволяют очищать кэш и поддерживать его размер, не давая ему расти бесконечно. Давайте рассмотрим некоторые из этих политик.
- First-In First-Out (FIFO) — Записи, которые были добавлены в кэш первыми, удаляются первыми. То есть, удаляются самые старые записи.
- Last-In First-Out (LIFO) — записи, которые добавляются в кэш последними или самыми новыми, удаляются первыми. То есть, удаляются самые новые записи.
- Наименее недавно использованные (LRU) — записи, которые не использовались в течение длительного времени, удаляются из кэша.
- Most Recently Used (MRU) — из кэша вытесняется запись, которая использовалась в последнее время.
- Наименее часто используемые (LFU) — записи, к которым обращаются не слишком часто, удаляются из кэша.
Давайте рассмотрим работу LRU немного подробнее. . .
Когда вы используете кэш со стратегией LRU, он упорядочивает элементы в соответствии с тем, как они используются пользователями.
Каждый раз, когда вы обращаетесь к записи в кэше, алгоритм LRU перемещает эту запись в верхнюю часть кэша (наиболее часто используемую).
Теперь, когда алгоритм просматривает кэш, он знает, что записи в самом низу устарели и больше не используются пользователями, поэтому их можно смело удалять из кэша, чтобы освободить место для новых записей.
Взгляните на следующее изображение.
Здесь, когда пользователь находит статью 1, кэш сохраняет эту статью как самую последнюю и затем предоставляет ее пользователю.
Теперь, когда пользователь запрашивает другую статью. . .
Кэш присваивает статье 2 статус самой последней и вытесняет статью 1 вниз по списку.
Таким образом, стратегия LRU определяет, какие записи должны быть удалены из кэша, чтобы можно было управлять размером кэша.
Давайте попробуем реализовать стратегию LRU на примере вычисления чисел Фибоначчи.
Ниже приведен простой код для вычисления чисел Фибоначчи
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Поскольку это рекурсивная функция, при большом значении n эта функция станет вычислительно тяжелой.
Давайте реализуем кэш для этой функции со стратегией LRU.
from functools import lru_cache
@lru_cache(maxsize=16)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
Аналогично тому, что мы видели ранее, мы можем использовать декоратор lru_cache из встроенной библиотеки functools для реализации кэша со стратегией выселения LRU.
Здесь параметр maxsize определяет максимальный размер кэша, до которого он может вырасти.
Теперь, если мы вызовем эту функцию и получим информацию о кэше, мы получим примерно такой результат.
Здесь
hits — количество вызовов, которые были возвращены из кэша
misses — количество вызовов, которых не было в кэше и которые пришлось вычислить
maxsize — максимальный размер кэша. Так как мы определили maxsize равным 16, в выводе будет показано то же самое.
currsize — текущий размер кэша. Здесь мы видим, что кэш заполнен.
Подводя итог
Кэширование не только сокращает вычислительные ресурсы системы, но и ускоряет работу кода.
Внедрение кэширования в ваш код может радикально улучшить пользовательский опыт, предоставляя быстрые и оптимальные результаты.
Источники
- https://www.youtube.com/watch?v=6FyXURRVmR0
- https://docs.python.org/3/library/functools.html
- https://realpython.com/lru-cache-python/