[Перевод] Стоит ли переходить с Python на Nim ради производительности?
Nim — это сочетание синтаксиса Python и производительности C
Несколько недель назад я бродил по GitHub и наткнулся на любопытный репозиторий: проект был полностью написан на языке Nim. До этого я с ним не сталкивался, и в этот раз решил разобраться, что это за зверь.
Сначала я подумал, что отстал от жизни, что это один из распространённых языков программирования, который многие, в отличие от меня, активно используют. И тогда я решил изучить его.
Вот какие выводы я сделал:
- Этот язык на самом деле популярен среди узкого круга лиц.
- Возможно, так и должно быть.
Итак, немного расскажу о моём опыте работы с Nim, коротко расскажу об особенностях программирования на нём, а также попробую сравнить его с Python и C. Забегая вперёд, отмечу, что этот язык кажется мне очень перспективным.
Код в студию!
В качестве примера я решил написать на Nim нечто более сложное, чем hello, world:
Вроде бы ничего лишнего, правда? Он кажется настолько простым, что вы без усилий сможете понять, что он делает, даже если вы никогда раньше не слышали о Nim. (Программа выведет 5 i: 5.)
Итак, давайте разберём то, что откуда-то кажется нам знакомым.
Объявление переменных
Это до боли знакомо разработчикам JavaScript. В то время как некоторые языки используют var, а некоторые используют let, JS и Nim позволяют использовать при объявлении переменных и то, и другое. Однако важно отметить, что в Nim они работают не так, как в JS. Но об этом позже.
Блоки
Чтобы обозначить новый блок в Nim, мы используем двоеточие, за которым следует строка с отступом. Всё, как в Python.
Ключевые слова
Оба цикла, а также оператор if выглядят так, как будто это фрагмент кода на Python. Фактически, всё начиная со строки 5 и далее является кодом на Python (при условии, что у нас определена функция echo).
Так что да, многие ключевые слова и операторы из Python также можно использовать в Nim: not, is, and, or и так далее.
То есть пока мы не видим в Nim ничего особенного: худшая версия Python (с точки зрения
синтаксиса), с учётом того, что для объявления переменных нужно использовать let или var.
На этом можно было бы остановиться, но есть большое «но»: Nim — статически типизированный язык, который работает почти так же быстро, как язык C.
Ну, теперь другой разговор. Давайте проверим это.
Тест производительности
Прежде чем углубиться в синтаксис Nim (особенно в статически типизированную часть, которую мы до сих пор не видели), давайте попробуем оценить его производительность. Для этого я написал наивную реализацию для вычисления n-го числа Фибоначчи в Nim, Python и C.
Чтобы всё было по-честному, я стандартизировал реализацию на основе решения Leetcode (Вариант 1) и постарался как можно строже придерживаться её на всех трёх языках.
Вы, конечно, можете напомнить мне про LRU Cache. Но сейчас моя задача — использовать стандартный подход, а не пытаться оптимизировать вычисления. Поэтому я выбрал наивную реализацию.
Вот результаты для вычисления 40-го числа Фибоначчи:
Да, строго говоря, нельзя назвать эксперимент чистым, но это коррелирует с результатами других энтузиастов, которые делали более серьёзные тесты [1][2][3].
Весь код, который я писал для этой статьи, доступен на GitHub, включая инструкцию о том, как провести этот эксперимент.
Так почему же Nim работает намного быстрее, чем Python?
Ну, я бы сказал, что есть две основные причины:
- Nim — компилируемый язык, а Python — интерпретируемый (подробнее об этом здесь). Это означает, что при запуске программы на Python выполняется больше работы, поскольку программу необходимо интерпретировать перед тем, как она сможет выполняться. Обычно из-за этого язык работает медленнее.
- Nim статически типизирован. Хотя в примере, который я показал ранее, не было ни одного объявления типа, позже мы увидим, что это действительно статически типизированный язык. В случае с Python, который динамически типизирован, интерпретатору нужно проделать гораздо больше работы, чтобы определить и соответствующим образом обработать типы. Это также снижает производительность.
Скорость работы растёт — скорость кодинга падает
Вот что в Python Docs говорится об интерпретируемых языках:
«Интерпретируемые языки обычно имеют более короткий цикл разработки / отладки, чем компилируемые, хотя их программы обычно работают медленнее».
Это хорошее обобщение компромисса между Python и C, например. Всё, что вы можете сделать на Python, вы можете сделать и на C, однако ваша программа на С будет работать во много раз быстрее.
Но вы будете тратить гораздо больше времени на написание и отладку своего кода на C, он будет громоздким и менее читабельным. И именно поэтому C уже не так востребован, а Python популярен. Другими словами, Python гораздо проще (сравнительно, конечно).
Итак, если Python находится на одном конце спектра, а C — на другом, то Nim пытается встать где-то посередине. Он работает намного быстрее, чем Python, но не так сложен для программирования, как C.
Давайте посмотрим на нашу реализацию вычисления чисел Фибоначчи.
С:
#include
int fibonacci(int n) {
if (n <= 1) {
return n;
}
return fibonacci(n-1) + fibonacci(n-2);
}
int main(void) {
printf("%i", fibonacci(40));
}
Python:
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(40))
Nim:
proc fibonacci(n: int): int =
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
echo(fibonacci(40))
Хотя у Nim в синтаксисе процедуры (функции) используется знак »=», в целом писать код намного проще, чем на C.
Может быть, это действительно достойный компромисс? Немного сложнее писать, чем на Python, но работает в десятки раз быстрее. Я мог бы с этим смириться.
Синтаксис Nim
import strformat
# Пример со страницы https://nim-lang.org/
type
Person = object
name: string
age: Natural # Возраст должен быть всегда положительным числом
let people = [
Person(name: "John", age: 45),
Person(name: "Kate", age: 30)
]
for person in people:
echo(fmt"{person.name} is {person.age} years old")
Просто укажу на ключевые особенности.
Переменные
Для объявления переменных используем var, let или const.
var и const работают так же, как в JavaScript, а с let — другая история.
let в JavaScript отличается от var с точки зрения области видимости, а let в Nim обозначает переменную, значение которой не может измениться после инициализации. Мне кажется, это похоже на Swift.
Но разве это не то же самое, что и константа? — спросите вы.
Нет. В Nim различие между const и let заключается в следующем:
Для const компилятор должен иметь возможность определять значение во время компиляции, тогда как для let оно может быть определено во время выполнения.
Пример из документации:
const input = readLine(stdin) # Error: constant expression expected
let input = readLine(stdin) # всё правильно
Кроме того, переменные можно объявлять и инициализировать так:
var
a = 1
b = 2
c = 3
x, y = 10 # Обе переменные x и y получили значение 10
Функции
Функции в Nim называются процедурами:
proc procedureName(parameterName: parameterType):returnType =
return returnVar
Учитывая, что язык во многом похож на Python, процедуры кажутся немного странными, когда вы впервые их видите.
Использовать »=» вместо »{» или »:» явно сбивает с толку. Всё обстоит немного лучше с записью процедуры в одну строку:
proc hello(s: string) = echo s
Вы также можете получать результат выполнения функции:
proc toString(x: int): string =
result =
if x < 0: "negative”
elif x > 0: "positive”
else: "zero”
Такое чувство, что всё равно нужно как-то вернуть result, но в данном случае result не является переменной — это ключевое слово. Так что, приведённый выше фрагмент кода будет правильным с точки зрения Nim.
Вы также можете перегружать процедуры:
proc toString(x: int): string =
result =
if x < 0: "negative"
elif x > 0: "positive"
else: "zero"
proc toString(x: bool): string =
result =
if x: "yep"
else: "nope"
echo toString(true) # Выведет "yep"
echo toString(5) # Выведет "positive"
Условия и циклы
Здесь много общего с Python.
# if true:
# while true:
# for num in nums:
Для перебора списка, например, вместо range () можно использовать countup (start, finish), или countdown (start, finish). Можно поступить ещё проще и использовать for i in start…finish
Пользовательский ввод и вывод
let input = readLine(stdin)
echo input
Если сравнивать с Python, то readLine (stdin) эквивалентно input (), а echo эквивалентно print.
echo можно использовать как со скобками, так и без них.
Моя задача — сделать так, чтобы у вас сложилось базовое представление о Nim, а не пересказывать всю его документацию. Поэтому я заканчиваю с синтаксисом и перехожу к другим особенностям языка.
Дополнительные особенности
Объектно-ориентированное программирование
Nim не объектно-ориентированный язык, но в нём реализована минимальная поддержка работы с объектами. До классов Python ему, конечно, далеко.
Макросы
Nim поддерживает макросы и метапрограммирование, и, кажется, разработчики достаточно сильно акцентируют на этом внимание. Этому посвящён собственный раздел серии из трёх уроков.
Маленький пример:
import macros macro myMacro(arg: static[int]): untyped =
echo arg
myMacro(1 + 2 * 3)
Базовые типы данных
string, char, bool, int, uint и float.
Также можно использовать эти типы:
int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64
Кроме того, в Nim строки являются mutable-типами, в отличие от Python.
Комментарии
В отличие от Python, в Nim для мультистрочных комментариев используется символ »#» в сочетании с »[» и »]».
# a comment#[
a
multi
line
comment
]#
Компиляция JavaScript
Nim может транслировать свой код в JavaScript. Не уверен, что многие заходят это использовать. Но есть вот такой пример браузерной игры «Змейка», написанной на Nim.
Итераторы
Итераторы в Nim больше похожи на генераторы в Python:
iterator countup(a, b: int): int =
var res = a
while res <= b:
yield res
inc(res)
Чувствительность к регистру и нижнее подчёркивание
Nim чувствителен только к регистру первого символа.
То есть, HelloWorld и helloWorld он различает, а helloWorld, helloworld и hello_world — нет. Поэтому без проблем будет работать, например, такая процедура:
proc my_func(s: string) =
echo myFunc("hello")
Насколько популярен Nim?
У Nim почти 10000 звезд на GitHub. Это явный плюс. Тем не менее, я попытался оценить популярность языка по другим источникам, и, конечно, она не так высока.
Например, Nim даже не упоминался в 2020 Stack Overflow Survey. Я не смог найти вакансии для разработчиков Nim в LinkedIn (даже с географией Worldwide), а поиск по тегу [nim-lang] на Stack Overflow выдал только 349 вопросов (сравните с ~ 1 500 000 для Python или с 270 000 для Swift)
Таким образом, было бы справедливо предположить, что большинство разработчиков не использовали его и многие никогда не слышали про язык Nim.
Замена Python?
Буду честен: я считаю Nim достаточно крутым языком. Чтобы написать эту статью, я изучил необходимый минимум, но этого хватило. Хотя я не слишком углублялся в него, я планирую использовать Nim в будущем. Лично я большой поклонник Python, но мне также нравятся языки со статической типизацией. Поэтому для меня улучшение производительности в некоторых случаях более чем компенсирует небольшую синтаксическую избыточность.
Хотя базовый синтаксис очень похож на Python, он более сложный. Поэтому большинство фанатов Python, скорее всего, не заинтересуются им.
Кроме того, не стоит забывать про язык Go. Я уверен, что многие из вас думали именно об этом во время чтения, и это правильно. Несмотря на то, что синтаксис Nim ближе к синтаксису Python, по производительности он конкурирует именно с языками а-ля «упрощённый C++».
Я в своё время тестировал производительность Go. В частности, для фибоначчи (40) он работал так же быстро, как C.
Но всё-таки: может ли Nim конкурировать с Python? Я очень сомневаюсь в этом. Мы наблюдаем тенденцию роста производительности компьютеров и упрощения программирования. И, как я уже отмечал, даже если Nim предложит хороший компромисс по соотношению синтаксиса и производительности, я не думаю, что этого достаточно, чтобы победить чистый и универсальный Python.
Я общался с одним из разработчиков Nim Core. Он считает, что Nim больше подходит для тех, кто переходит с C ++, чем для питонистов.
Может ли Nim конкурировать с Go? Возможно (если Google «разрешит»). Язык Nim такой же мощный, как и Go. Более того, в Nim лучше реализована поддержка функций C / C ++, в том числе макросы и перегрузка.
Но об этом как-нибудь в следующий раз.
На правах рекламы
Эпичные серверы — это доступные виртуальные серверы с процессорами от AMD, частота ядра CPU до 3.4 GHz. Максимальная конфигурация позволит оторваться на полную — 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe. Поспешите заказать!