В этой статье поговорим о том, как можно ускорить свой Python код при помощи библиотек, скомпилированных с помощью Nim.

Также узнаем, какие библиотеки на Python написаны с помощью Nim и даже напишем свой небольшой модуль.

Обложка статьи
Обложка статьи

Подготовка

Для того, чтобы начать - необходимо поставить Nim. Если он у вас уже есть - отлично, идем дальше.

Все действия ниже будут производиться с Nim 2.0.0 и с Python 3.10.9.

С помощью пакетного менеджера nimble ставим пакет nimpy, с помощью которого мы сможем разрабатывать Python библиотеки на Nim.

nimble install nimpy

Переходим к Python и возьмем какой-нибудь алгоритм для замера производительности, например фибоначчи. Создадим файл fib.py и напишем саму функцию.

def fib(n: int) -> int:
    if n == 0:
        return 0
    elif n < 3:
        return 1
    return fib(n - 1) + fib(n - 2)

Теперь вернемся к Nim и создадим файл nimfib.nim. Как это будет выглядеть здесь?

import nimpy  # импортируем библиотеку nimpy

# Объявляем функцию fib(n) 
func fib(n: int): int {.exportpy.} =
  if n == 0:
    return 0
  elif n < 3:
    return 1
  return fib(n - 1) + fib(n - 2)

Выглядит действительно схоже, не так ли? Попробуем скомпилировать в python библиотеку:

nim c -o:nimfib.pyd --tlsEmulation:off --passL:-static --threads:on --app:lib -d:danger --opt:speed nimfib

Эту команду можно вынести в отдельный файл.

Для Unix систем команда выше выглядит следующим образом:

nim c -o:nimfib.so --app:lib -d:danger --threads:on --opt:speed nimfib

При компиляции с помощью --threads:on Nim будет подставлять--tlsEmulation:on (только для Windows), что предотвращает правильную инициализацию среды выполнения Nim при вызове из внешнего потока (что всегда имеет место в случае модуля Python).

Теперь посмотрим, насколько быстро работают Nim и Python. Для этого ставим пакеты pytest и pytest-benchmark.

pip install pytest pytest-benchmark

Создадим файл main.py:

from timeit import default_timer
import pytest
import fib
import nimfib


@pytest.mark.benchmark(group="fibonacci", timer=default_timer)
def test_py_fib(benchmark):
    result = benchmark(fib.fib, 35)

@pytest.mark.benchmark(group="fibonacci", timer=default_timer)
def test_nim_fib(benchmark):
    result = benchmark(nimfib.fib, 35)

Теперь запустим это через pytest:

pytest main.py

А вот и результаты:

--------------------------------------------------------------------------------- benchmark 'fibonacci': 2 tests ---------------------------------------------------------------------------------
Name (time in ms)            Min                   Max                  Mean             StdDev                Median                IQR            Outliers     OPS            Rounds  Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_nim_fib             93.4353 (1.0)        117.2837 (1.0)        102.3561 (1.0)       7.9790 (1.0)         99.2676 (1.0)      11.3069 (1.0)           3;0  9.7698 (1.0)           9           1
test_py_fib           2,986.7212 (31.97)    3,013.6137 (25.70)    2,998.6946 (29.30)    11.7603 (1.47)     3,001.7616 (30.24)    20.1307 (1.78)          3;0  0.3335 (0.03)          5           1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Legend:
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
====================================================================== 2 passed in 23.19s =======================================================================

Как вы можете видеть - Nim, скомпилированный в C быстрее Python в 30 раз.

Если мы скомпилируем Nim в C++ (заменив nim c на nim cpp), то получим уже следующие результаты:

--------------------------------------------------------------------------------- benchmark 'fibonacci': 2 tests ---------------------------------------------------------------------------------
Name (time in ms)            Min                   Max                  Mean            StdDev                Median               IQR            Outliers       OPS            Rounds  Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_nim_fib              9.4439 (1.0)         14.0821 (1.0)          9.7844 (1.0)      0.7489 (1.0)          9.5708 (1.0)      0.1119 (1.0)          7;15  102.2032 (1.0)         106           1
test_py_fib           3,003.1009 (317.99)   3,016.5476 (214.21)   3,009.6761 (307.60)   5.4503 (7.28)     3,010.4404 (314.55)   8.8961 (79.50)         2;0    0.3323 (0.00)          5           1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Legend:
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean
====================================================================== 2 passed in 23.19s ======================================================================= 

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

Перейдем к более реальным примерам


Готовые библиотеки

Есть несколько реальных примеров использования nimpy для разработки Python библиотек:


Кошки, собаки и Python классы

Давайте попробуем создать на стороне Nim объект Entity. Создадим файл entity.nim:

import nimpy
import strformat


type
  Entity* = ref object of PyNimObjectExperimental
    health: int
    maxHealth: int
    damage: int
    name: string


proc initEntity*(name: string, health: int, damage: int = 1): Entity {.exportpy: "create_entity".} =
  return Entity(
    name: name,
    health: health,
    maxHealth: health,
    damage: damage
  )


proc isAlive*(self: Entity): bool {.exportpy: "is_alive".} =
  return self.health > 0


proc hit*(self: Entity, other: Entity) {.exportpy.} =
  # Примитивная логика получения удара
  other.health -= self.damage
  if other.health <= 0:
    echo fmt"{other.name} погиб в бою от руки {self.name} ????"
  else:
    echo fmt"{other.name} получает {self.damage} урона от {self.name} ⚔"


proc name*(self: Entity): string {.exportpy.} =
  return self.name

Теперь скомпилируем:

nim c -o:entity.pyd --tlsEmulation:off --passL:-static --app:lib entity

И попробуем в Python:

import entity


dog = entity.create_entity("Собака", 10)
cat = entity.create_entity("Кот", 4, 2)


while dog.is_alive() and cat.is_alive():
    dog.hit(cat)
    cat.hit(dog)

if dog.is_alive():
    print(f"{dog.name()} одержал победу!")
else:
    print(f"{cat.name()} одержал победу!")

Как можно заметить - при создании собаки мы отправили лишь 2 аргумента из трех, потому что в create_entity у аргумента damage есть значение по умолчанию.

Запускаем и видим результат:

Кот получает 1 урона от Собака ⚔
Собака получает 2 урона от Кот ⚔
Кот получает 1 урона от Собака ⚔
Собака получает 2 урона от Кот ⚔
Кот получает 1 урона от Собака ⚔
Собака получает 2 урона от Кот ⚔
Кот погиб в бою от руки Собака ????
Собака получает 2 урона от Кот ⚔
Собака одержал победу!

Заключение

Да, с помощью Nim можно создавать модули для Python, однако нужно понимать, что не для всего подойдет расширение на C/C++. В основном это какие-то низкоуровневые операции, работа с потоками и прочие cpu-bound вычисления.

Комментарии (14)


  1. mOlind
    08.12.2023 14:17

    Если уж ускорять, то с mojo????


    1. palyaros02
      08.12.2023 14:17

      Поскорее бы дождаться релиза


      1. ValeryIvanov
        08.12.2023 14:17

        Зачем ждать релиза Mojo, когда Cython уже существует?


  1. Jury_78
    08.12.2023 14:17

    Почему такое отличие Си от Си++ ?


    1. akihayase
      08.12.2023 14:17

      возможно у с++ какие-то оптимизации, которые с не делает, да и надо глубже в сам Nim копать, точно сказать не могу.


  1. funny_falcon
    08.12.2023 14:17

    А почему в вариантах нет Cython?


    1. akihayase
      08.12.2023 14:17

      Неправильно написал, поправил)


  1. blackmius
    08.12.2023 14:17

    хорошая статья, не знал что га ниме действительно так просто сделать биндинги на питон. кстати, вопрос про биндинги к биндингам: есть ли возможность сделать ffi к библиотеки на си, а потом подключить это к питону, чтобы не мучаться с типами cython?


    1. akihayase
      08.12.2023 14:17

      Конечно можно.
      Возьмем в пример C-функцию printf:

      import nimpy
      
      proc printf(formatstr: cstring) {.importc: "printf", varargs,
                                        header: "<stdio.h>".}
      
      proc callPrintF(formatstr: string) {.exportpy: "printf".} =
        printf(cstring(formatstr))

      А затем скомпилируем и вызовем printf на стороне Python :)

      import mybinds
      
      mybinds.printf("hello, world!")


  1. Apoheliy
    08.12.2023 14:17

    Автор остановился на половине пути, грустно.

    Отлично было бы добавить вишенку на тортик: пишем библиотеку на плюсах, цепляем её в питони и смотрим соотношение nim и чистых плюсов!


    1. akihayase
      08.12.2023 14:17

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


  1. megazhuk
    08.12.2023 14:17

    Не увидел, что-бы кто-то сказал хоть слово про мypyc, тогда я скажу.
    https://github.com/mypyc/mypyc

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


    1. akihayase
      08.12.2023 14:17

      Не знаком с mypy, но выглядит интересно, спасибо за комментарий.


  1. alexandr_domanskiy
    08.12.2023 14:17

    Если нужно добавить скорость вычислений в Python, то Rust вам в помощь.