В этой статье поговорим о том, как можно ускорить свой 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 библиотек:
faster-than-requests (исходный код) - тот же requests, но написан на Nim.
faster-than-csv (исходный код) - тот же csv, но на Nim.
nimporter (исходный код) - утилита, с помощью которой можно импортировать Nim файлы напрямую в Python. Компиляция происходит автоматически.
HappyX (исходный код) - веб-фреймворк, написанный на Nim и доступный как для Python, так и для NodeJS и JVM.
pyMeow (исходный код) - библиотека для написания читов с помощью Raylib, Python и Nim.
Кошки, собаки и 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)
blackmius
08.12.2023 14:17хорошая статья, не знал что га ниме действительно так просто сделать биндинги на питон. кстати, вопрос про биндинги к биндингам: есть ли возможность сделать ffi к библиотеки на си, а потом подключить это к питону, чтобы не мучаться с типами cython?
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!")
Apoheliy
08.12.2023 14:17Автор остановился на половине пути, грустно.
Отлично было бы добавить вишенку на тортик: пишем библиотеку на плюсах, цепляем её в питони и смотрим соотношение nim и чистых плюсов!
akihayase Автор
08.12.2023 14:17Nim использует различные оптимизации при компиляции на таргет языки, поэтому соотношение по скорости там будет весьма маленькое, либо его вообще не будет.
megazhuk
08.12.2023 14:17Не увидел, что-бы кто-то сказал хоть слово про мypyc, тогда я скажу.
https://github.com/mypyc/mypycОснован на линтере mypy, который жёстко заставляет типизировать ваш код. Как я понимаю, именно благодаря этому, учитывая типы и заставляя согласовывать их везде он добивается своей цели.
На выходе выдаёт *.dll ку, которая потом легко подключается обратно в код через import.
Его использовал не сколько для ускорения (не делал даже бенчмаки), сколько для того, чтобы скрыть код на Питоне, когда надо было на удаленную машину клиенту ставить и проводить предварительную демонтстрацию функционала.
alexandr_domanskiy
08.12.2023 14:17Если нужно добавить скорость вычислений в Python, то Rust вам в помощь.
mOlind
Если уж ускорять, то с mojo????
palyaros02
Поскорее бы дождаться релиза
ValeryIvanov
Зачем ждать релиза Mojo, когда Cython уже существует?