- PVSM.RU - https://www.pvsm.ru -
Иногда тормоза в коде выглядят как что-то сложное: тяжёлые алгоритмы, огромные базы данных, медленный диск. Но чаще всё намного банальнее — один неудачный цикл, который выполняется миллионы раз.

Недавно я столкнулся именно с такой ситуацией. Нужно было обработать большой лог-файл — около 8 ГБ — и извлечь из него статистику по пользователям. Скрипт работал почти 9 минут, хотя логика казалась предельно простой.
Разбор показал, что проблема была в одном месте.
Есть большой лог:
timestamp user_id action
Например:
1690039200 421 login
1690039211 421 view
1690039220 182 login
1690039230 421 logout
Нужно посчитать количество действий каждого пользователя.
Наивная реализация выглядела примерно так.
def count_actions(file_path):
users = []
with open(file_path) as f:
for line in f:
user_id = line.split()[1]
found = False
for u in users:
if u["id"] == user_id:
u["count"] += 1
found = True
break
if not found:
users.append({"id": user_id, "count": 1})
return users
Код простой и понятный. Он даже работает корректно.
Но есть нюанс.
Проблема в этом фрагменте.
for u in users:
if u["id"] == user_id:
Мы ищем пользователя линейным поиском.
Если пользователей много, сложность становится:
O(n²)
Почему так происходит.
Для каждой строки:
читаем user_id
перебираем весь список пользователей
проверяем совпадение
Если строк миллионы, этот цикл начинает работать катастрофически медленно.
Я взял лог на 10 миллионов строк.
Результат выполнения:
8 минут 47 секунд
Процессор почти всё время был загружен.
При профилировании стало видно, где происходит основная потеря времени.
import cProfile
cProfile.run("count_actions('log.txt')")
Самая тяжёлая функция — поиск пользователя в списке.
Очевидная оптимизация — заменить список на словарь.
Почему.
Словарь Python реализован как хеш-таблица, поэтому поиск происходит за:
O(1)
Перепишем код.
def count_actions(file_path):
users = {}
with open(file_path) as f:
for line in f:
user_id = line.split()[1]
if user_id in users:
users[user_id] += 1
else:
users[user_id] = 1
return users
Теперь мы не перебираем всех пользователей.
Мы просто обращаемся к словарю по ключу.
Запускаем тот же лог.
12 секунд
Скрипт ускорился примерно в 42 раза.
Без изменения алгоритма обработки файла.
Без многопоточности.
Без C-расширений.
Просто убрали один цикл.
Да. В Python есть ещё более удобный инструмент — defaultdict.
Он избавляет от проверки наличия ключа.
from collections import defaultdict
def count_actions(file_path):
users = defaultdict(int)
with open(file_path) as f:
for line in f:
user_id = line.split()[1]
users[user_id] += 1
return users
Код становится не только быстрее, но и чище.
split() разбивает всю строку.
Но нам нужен только второй элемент.
Можно использовать split с ограничением.
user_id = line.split(" ", 2)[1]
Это уменьшает количество создаваемых объектов.
На больших файлах это тоже заметно.
from collections import defaultdict
def count_actions(file_path):
users = defaultdict(int)
with open(file_path) as f:
for line in f:
parts = line.split(" ", 2)
user_id = parts[1]
users[user_id] += 1
return users
|
версия |
время |
|---|---|
|
наивная |
8 мин 47 сек |
|
словарь |
12 сек |
|
defaultdict |
10 сек |
Разница — примерно 50×.
Есть несколько причин.
На файле из 1000 строк разница почти незаметна.
Проблема проявляется только на больших данных.
Логика читается легко.
Но читаемость и сложность алгоритма — разные вещи.
Например:
x in list
выглядит как одна операция, но внутри происходит перебор.
Есть простой сигнал.
Если в коде есть конструкция:
цикл внутри цикла
и количество данных может расти — почти всегда есть более эффективный способ.
Чаще всего это:
словари
множества
индексы
предварительная агрегация
Иногда ускорение программы не требует сложных оптимизаций.
Достаточно задать один вопрос:
Какая сложность у этого участка кода?
В моём случае всё ускорилось в десятки раз после удаления одного цикла.
И это одна из самых частых причин медленных скриптов, которые я встречаю в Python-проектах.
Интересно, что почти все медленные Python-скрипты, которые я видел в реальных проектах, тормозили не из-за Python.
Проблема почти всегда была в алгоритмах.
Чаще всего встречаются три вещи:
линейный поиск в списке
лишние вложенные циклы
повторные вычисления, которые можно было кешировать
Python в таких случаях просто честно выполняет то, что ему написали.
Поэтому мне стало интересно:
какая самая странная оптимизация кода давала вам самый большой прирост скорости?
Иногда ведь достаточно удалить одну строчку — и программа начинает работать на порядок быстрее.
Делитесь примерами, думаю получится интересная коллекция инженерных историй.
Автор: rinciabjelar
Источник [1]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/446978
Ссылки в тексте:
[1] Источник: https://habr.com/ru/articles/1011582/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1011582
Нажмите здесь для печати.