В этой статье поговорим о том, как можно ускорить свой 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}