Как я улучшил производительность своего кода Python на 371%?

Как я улучшил производительность своего кода Python на 371% от 29,3 секунд до 6,3 без какой-либо внешней библиотеки?
Введение
Прежде чем приступить к работе, давайте обсудим постановку задачи. Я хотел проанализировать некоторые данные, хранящиеся в текстовом файле. Каждая строка содержала четыре числовых значения, разделённых пробелом (всего 46,66 млн строк). Размер файла составляет около 1,11 ГБ, и я прилагаю небольшой снимок экрана с данными ниже, чтобы вы поняли, как это выглядит:

Мне нужно было извлечь только строки для заданного значения третьего столбца (3100.10 на изображении выше). Первое, что я попробовал, это просто использовать numpy.genfromtxt(), но это выдало ошибку памяти, поскольку данные слишком велики для обработки в один заход.
Я попытался разбить данные на более мелкие фрагменты и сделать то же самое, но это было мучительно медленно 😫, поэтому я пробовал разные способы, чтобы выполнить работу как можно быстрее. Я покажу вам код вместе с концепциями, которые я использовал для оптимизации своего кода:
def Function1():
output="result.txt"
output_file=open(output, "w")
value=3100.10
with open(file, "r") as f:
for line in f:
if(line!="\n"):
if(len(line.split(" "))==4):
try:
if(int(float(line.split(" ")[2]))==int(value)):
output_file.write(line)
except ValueError:
continue
f.close()
output_file.close()
Отправная точка
Это самый простой подход к решению этой проблемы. Переберите весь файл построчно, проверьте, содержит ли строка это значение, если да, то добавьте строку в новый файл.
Код занял 29,3 с ± 56,7 мс на цикл (среднее значение ± стандартное отклонение из 3 прогонов, по 1 циклу в каждом)
1. Инвариант цикла
Первый шаг оптимизации — посмотреть на код и понять, делаем ли мы в нём что-то совсем не нужное. В цикле, где я перебираю строки, я использую int(value) для сравнения значения. Этого можно избежать, преобразовав значение в int один раз и используя его в цикле. Это называется инвариантом цикла, когда мы снова и снова делаем что-то в цикле, чего можно избежать.
Подробнее об этом можно прочитать здесь
Вот код:
def Function1():
output="result.txt"
output_file=open(output, "w")
value=3100.10
with open(file, "r") as f:
for line in f:
if(line!="\n"):
if(len(line.split(" "))==4):
try:
if(int(float(line.split(" ")[2]))==int(value)):
output_file.write(line)
except ValueError:
continue
f.close()
output_file.close()
Код занял 27,5 с ± 264 мс на цикл (среднее значение ± стандартное отклонение для 3 прогонов, по 1 циклу в каждом).
Изменив одну строку кода, он получил прирост производительности на 6,5% по сравнению с предыдущим кодом. Это очень распространённая ошибка, которую допускают программисты.
2. Отображение файла в память
Это метод, при котором мы загружаем весь файл в память (ОЗУ), что намного быстрее, чем обычный файловый ввод-вывод. Обычный ввод-вывод использует несколько системных вызовов для чтения данных с диска и возврата их в программу через несколько буферов данных. Отображение файла в память пропускает эти шаги и копирует данные в память, что приводит к повышению производительности (в большинстве случаев).
Python имеет модуль с именем «mmap» , который используется для этой цели.
Вот код:
def Function3():
output="result.txt"
output_file=open(output, "wb")
value=int(3100.10)
with open(file, "r+b") as f:
mmap_file=mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
for line in iter(mmap_file.readline, b""):
if(line!=b"\n"):
if(len(line.split(b" "))==4):
try:
if(int(float(line.split(b" ")[2]))==value):
output_file.write(line)
except ValueError:
continue
mmap_file.flush()
f.close()
output_file.close()
Код занял 22,8 с ± 124 мс на цикл (среднее значение ± стандартное отклонение из 3 прогонов, по 1 циклу в каждом).
Его производительность увеличилась на 20% по сравнению с предыдущим кодом.
3. Использование нерезания вместо преобразования типов данных
В строке int(float(line.split(b” “)[2]))==value я разрезаю строку, чтобы получить третий элемент, а затем преобразовываю строку в float, и в int для сравнения.
Это выглядит как «0 3098 3100,10 56188» -> «3100,10:-> 3100,10-> 3100
Теперь вместо использования float, а затем int для преобразования строки с десятичным числом в целое число, я использовал нарезку, что привело к огромному приросту производительности, поскольку операции со строками выполняются быстрее, чем преобразования данных.
Вот код:
def Function4():
output="result.txt"
output_file=open(output, "wb")
value=int(3100.10)
with open(file, "r+b") as f:
mmap_file=mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
for line in iter(mmap_file.readline, b""):
if(line!=b"\n"):
if(len(line.split(b" "))==4):
try:
if(int(line.split(b" ")[2][:-3])==value):
output_file.write(line)
except ValueError:
continue
mmap_file.flush()
f.close()
output_file.close()
На этот раз код занял 20 с ± 171 мс на цикл (среднее значение ± стандартное отклонение для 3 прогонов, по 1 циклу в каждом).
Его производительность увеличилась на 14% по сравнению с предыдущим кодом только за счёт изменения одной строки кода.
4. Использование операции поиска
До этого времени я перебирал строки, извлекая значение 3-го столбца и сравнивая его. На этот раз я использую операцию поиска для поиска нужного значения в каждой строке. И это удивительно быстро работает!
Вы можете прочитать больше об этом здесь
Вот код:
def Function5():
output="result.txt"
output_file=open(output, "wb")
value=int(3100.10)
value=(str(value)+".").encode()
with open(file, "r+b") as f:
mmap_file=mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
for line in iter(mmap_file.readline, b""):
find=line.find(value)
if(find>=7 and find<=11):
output_file.write(line)
mmap_file.flush()
f.close()
output_file.close()
На этот раз код занял 6,22 с ± 55,8 мс на цикл (среднее значение ± стандартное отклонение из 3 прогонов, по 1 циклу в каждом).
Это увеличило производительность на 221,5% по сравнению с предыдущим кодом.
Это почти в 4,7 раза быстрее, чем мы начали.
Используемое оборудование: –
> Legion 5 15ACH6
-> AMD 5800H
-> 16 ГБ ОЗУ
Ниже вы можете увидеть сравнение между Windows 10 и Ubuntu (22.04 LTS)!
Уровень оптимизации WindowsLinux029.318.2127.517.3222.819.332018.946.228.88

Надеюсь, что данная статья окажется для вас полезной!