Оптимизация кода на Python с помощью ctypes

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
Перевод статьи подготовлен специально для студентов курса «Разработчик Python».



Внимание: код в этой статье лицензирован под GNU AGPLv3.

Я написал это руководство, поскольку не смог найти такого, которое будет объединять в себе все полезное о ctypes. Надеюсь, эта статья сделает чью-то жизнь намного легче.

Содержание:

  1. Базовые оптимизации
  2. сtypes
  3. Компиляция под Python
  4. Структуры в Python
  5. Вызов вашего кода на С
  6. PyPy

Базовые оптимизации


Перед переписыванием исходного кода Python на С, рассмотрим базовые методы оптимизации в Python.

Встроенные структуры данных


Встроенные структуры данных в Python, такие как set и dict, написаны на С. Они работают гораздо быстрее, чем ваши собственные структуры данных, написанные как классы Python. Другие структуры данных, помимо стандартных set, dict, list и tuple описаны в документации модуля collections.

Списочные выражения


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

# Slow
      mapped = []
      for value in originallist:
          mapped.append(myfunc(value))
      
      # Faster
      mapped = [myfunc(value) in originallist]

ctypes


Модуль ctypes позволяет вам взаимодействовать с кодом на С из Python без использования модуля subprocess или другого подобного модуля для запуска других процессов из CLI.

Тут всего две части: компиляция кода на С для загрузки в качестве shared object и настройка структур данных в коде на Python для сопоставления их с типами С.

В этой статье я буду соединять свой код на Python с lcs.c, которая находит самую длинную подпоследовательность в двух списках строк. Я хочу, чтобы в Python работало следующее:

list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!']
      list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.']
      
      common = lcs(list1, list2)
      
      print(common)
      # ['My', 'name', 'is', 'Stevens']

Одна из проблем заключается в том, что эта конкретная функция на С — это сигнатура функции, которая принимает списки строк в качестве типов аргументов, и возвращает тип, у которого нет фиксированной длины. Я решаю эту задачу с помощью структуры последовательности, содержащей указатели и длину.

Компиляция кода на С под Python


Сначала, исходный код на С (lcs.c) компилируется в lcs.so, чтобы загружаться в Python.

gcc -c -Wall -Werror -fpic -O3 lcs.c -o lcs.o
      gcc -shared -o lcs.so lcs.o

  • -Wall отобразит все предупреждения;
  • -Werrow обернет все предупреждения в ошибки;
  • -fpic сгенерирует независимые от положения инструкции, которые понадобятся, если вы захотите использовать эту библиотеку в Python;
  • -O3 максимизирует оптимизацию;

А теперь мы начнем писать код на Python, используя полученный файл shared object.

Структуры в Python


Ниже представлены две структуры данных, которые используются в моем коде на С.

struct Sequence
      {
          char **items;
          int length;
      };
      
      struct Cell
      {
          int index;
          int length;
          struct Cell *prev;
      };

А вот перевод этих структур на Python.

import ctypes
      class SEQUENCE(ctypes.Structure):
          _fields_ = [('items', ctypes.POINTER(ctypes.c_char_p)),
                      ('length', ctypes.c_int)]
      
      class CELL(ctypes.Structure):
          pass
      
      CELL._fields_ = [('index', ctypes.c_int), ('length', ctypes.c_int),
                       ('prev', ctypes.POINTER(CELL))]

Несколько заметок:

  • Все структуры – это классы, наследуемые от ctypes.Structure.
  • Единственное поле _fields_ представляет из себя список кортежей. Каждый кортеж это (<variable-name>, <ctypes.TYPE>).
  • В ctypes есть похожие типы c_char (char) и c_char_p (*char).
  • В ctypes также есть POINTER(), который создает указатель типа из каждого типа ему переданного.
  • Если у вас есть рекурсивное определение, как в CELL, вы должны передать начальное объявление, а затем добавить поля _fields_, чтобы позже получить ссылку на себя же.
  • Поскольку я не использовал CELL в своем коде на Python, мне не нужно было писать эту структуру, но у нее есть интересное свойство в рекурсивном поле.

Вызов вашего кода на С


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

def list_to_SEQUENCE(strlist: List[str]) -> SEQUENCE:
          bytelist = [bytes(s, 'utf-8') for s in strlist]
          arr = (ctypes.c_char_p * len(bytelist))()
          arr[:] = bytelist
          return SEQUENCE(arr, len(bytelist))
      
      
      def lcs(s1: List[str], s2: List[str]) -> List[str]:
          seq1 = list_to_SEQUENCE(s1)
          seq2 = list_to_SEQUENCE(s2)
      
          # struct Sequence *lcs(struct Sequence *s1, struct Sequence *s2)
          common = lcsmodule.lcs(ctypes.byref(seq1), ctypes.byref(seq2))[0]
      
          ret = []
      
          for i in range(common.length):
              ret.append(common.items[i].decode('utf-8'))
          lcsmodule.freeSequence(common)
      
          return ret
      
      lcsmodule = ctypes.cdll.LoadLibrary('lcsmodule/lcs.so')
      lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE)
      
      list1 = ['My', 'name', 'is', 'Sam', 'Stevens', '!']
      list2 = ['My', 'name', 'is', 'Alex', 'Stevens', '.']
      
      common = lcs(list1, list2)
      
      print(common)
      # ['My', 'name', 'is', 'Stevens']

Немного заметок:

  • **char (список строк) проводит сопоставление напрямую в список байт в Python.
  • В lcs.c есть функция lcs() с сигнатурой struct Sequence *lcs(struct Sequence *s1, struct Sequence *s2). Чтобы настроить возвращаемый тип, я использую lcsmodule.lcs.restype = ctypes.POINTER(SEQUENCE).
  • Чтобы сделать вызов с ссылкой на структуру Sequence, я использую ctypes.byref(), который возвращает «легкий указатель» на ваш объект (быстрее, чем ctypes.POINTER()).
  • common.items – это список байт, они могут быть декодированы для получения ret в виде списка str.
  • lcsmodule.freeSequence(common) просто освобождает память, ассоциированную с common. Это важно, поскольку сборчик мусора (AFAIK) его не соберет автоматически.

Оптимизированный код на Python – это код, который вы написали на С и обернули в Python.

Кое-что еще: PyPy


Внимание: Сам я никогда PyPy не пользовался.
Одна из самых простых оптимизаций заключается в запуске ваших программ в среде выполнения PyPy, которая содержит в себе JIT-компилятор (just-in-time), который ускоряет работу циклов, компилируя их в машинный код при многократном выполнении.

Если у вас появятся комментарии или вы захотите что-то обсудить, напишите мне (samuel.robert.stevens@gmail.com).

На этом все. До встречи на курсе!
Источник: https://habr.com/ru/company/otus/blog/490244/


Интересные статьи

Интересные статьи

Во время Чемпионата мира по регби в 2019 году я сделал небольшой научный проект Data Science, чтобы попытаться спрогнозировать результаты матчей, написав о нем здесь. Я развил проект до п...
Всем привет, меня зовут Фёдор Индукаев, я работаю аналитиком в Яндекс.Маршрутизации. Сегодня хочу рассказать вам про задачу визуализации пересекающихся множеств и про пакет для Pytho...
Мы всегда хотим писать код быстро, но за это приходится платить. На обычных высокоуровневых гибких языках можно быстро разрабатывать программы, но после запуска они работают медленно. Например, ч...
Вот бывает же в жизни такое. Сидишь себе не шалишь, никого не трогаешь, починяешь примус, а тут из этого примуса, из телевизора, да и вообще из каждого утюга, до тебя доносится: «нейронные сети, ...
Мы в JSOC CERT ежедневно сталкиваемся с событиями из разных песочниц, которые функционируют в составе AntiAPT-решений наших заказчиков и пропускают через себя тысячи файлов из web- и почтового тр...