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

Как я улучшил производительность своего кода Python на 371% от 29,3 секунд до 6,3 без какой-либо внешней библиотеки?

Введение

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

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

Мне нужно было извлечь только строки для заданного значения третьего столбца (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

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

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

+1
0
+1
2
+1
0
+1
0
+1
1

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *