Простая стратегия повышения производительности вашего программного обеспечения.

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

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

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

Что такое кеширование?

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

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

Ситуации, в которых вы можете захотеть использовать кеш:

  • Запрос к базе данных может быть довольно сложным и медленным, поэтому лучше сохранить его ответ. Вы экономите время и освобождаете свою базу данных для выполнения других запросов.
  • Загрузка файла по URL-адресу может быть медленной. В настоящий момент у вас плохое соединение, файл может быть огромным или сервер просто занят. Кеширование может сэкономить ваше время (и деньги).
  • Интенсивные вычисления, как и рекурсивные алгоритмы, могут использовать одни и те же шаги несколько раз. Сохранение промежуточных результатов вместо того, чтобы вычислять их каждый раз, повысит производительность вашего приложения.

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

Использование словаря в качестве кеша

Самый простой способ реализовать кеш в Python - использовать словарь в качестве хранилища значений ключей.

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

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

Возьмем пример. Мы хотим скачать несколько фотографий, чтобы показать нашим друзьям.

В этом случае каждый раз, когда мы хотим показать, мы загружаем фотографию с URL-адреса. Если вы запустите этот код, вы можете получить что-то вроде этого:

$ python fetch.py
Fetched data for 1 in 1.4536 s
Fetched data for 2 in 1.4139 s
Fetched data for 1 in 1.4503 s
Fetched data for 2 in 1.5367 s
Fetched data for 1 in 1.4323 s
Fetched data for 1 in 1.5136 s
Fetched data for 2 in 1.4559 s

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

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

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

Когда мы его запускаем:

$ python fetch_cache_dict.py
Fetched data for 1 in 2.3582 s
Fetched data for 2 in 2.0616 s
Fetched data for 1 in 0.0000 s
Fetched data for 2 in 0.0000 s
Fetched data for 1 in 0.0000 s
Fetched data for 1 in 0.0000 s
Fetched data for 2 in 0.0000 s

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

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

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

Кэш LRU

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

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

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

К счастью, начиная с версии Python 3.2, мы можем использовать lru_cache из модуля functools.

Вы можете украсить функцию, которую хотите кэшировать, и установить для нее максимальный размер.

Как мы его запускаем:

$ python fetch_lru_cache.py
Fetched data for 1 in 2.1913 s
Fetched data for 2 in 1.8985 s
Fetched data for 3 in 2.6830 s
Fetched data for 1 in 0.0000 s
Fetched data for 1 in 0.0000 s
Fetched data for 4 in 2.6950 s
Fetched data for 2 in 1.9950 s
Fetched data for 2 in 0.0000 s

Давайте проанализируем результат:

  • Первые три запроса нормальные. Поскольку у нас нет ни одного идентификатора в кеше, мы должны получить их по URL-адресу.
  • Когда мы снова пытаемся получить ID 1, поскольку мы уже запрашивали его, кеш просто возвращает его нам.
  • Поскольку у нас максимальный размер трех элементов, когда мы пытаемся получить четвертый (ID 4), один из них должен быть отброшен.
  • Элемент 1 был получен трижды, а элемент 2 и 3 - только один. Поскольку элемент 2 был вставлен раньше, он отбрасывается.
  • И мы можем увидеть это, когда снова попытаемся получить элемент 2.

Если вам нужен более глубокий анализ кеша LRU, прочтите эту статью:



Помимо кеша, вы можете проверить некоторую информацию о нем, обратившись к функции cache_info(). В нашем случае это будет download.cache_info():

print(download.cache_info())
CacheInfo(hits=3, misses=5, maxsize=3, currsize=3)

Он содержит четыре свойства кеша:

  • hits: сколько раз значение было найдено в кеше.
  • misses: сколько раз значение не было найдено в кеше.
  • maxsize: максимальное количество элементов в кеше (установленное вами).
  • currsize: текущее количество элементов внутри кеша.

Вы можете использовать функцию download.cache_clear() для очистки всего кеша.

Заключение

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

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

Это экономит время и может значительно улучшить взаимодействие с пользователем.

Спасибо за чтение!