Мой собственный способ измерить выразительность и скорость языка — решить на нём “задачу Джеймса Бонда младшего”, выдуманную головоломку из игры на NES James Bond Jr
(видео). Несмотря на игрушечность задачи, кажется, это неплохой тест нового языка. Это веселее, чем реализовывать абстрактный поиск в ширину/глубину. Задачу решаю с небольшими алгоритмическими оптимизациями, но без оптимизаций по мелочам (скорее, наоборот, массив чисел специально копируется, как решена первая версия задачи на питоне, чтобы решения были сравнивыми, чтобы симулировать “код новичка” на языка и посмотреть, как язык справляется с этим копированием).
Сразу выводы про daScript
для тех, кому не особенно интересны подробности реализации:
— Выразительность языка ОЧЕНЬ похожа на Python
. Более того, я фактически просто переписал своё решение на Python
13-летней давности построчно, с парой изменений.
— daScript
по скорости в режиме интерпретации находится в одной лиге с компилируемыми языками (!!!). Код по скорости сопоставим с версией на Nim
(чуть быстрее “наивной” скомпилированной версии, и раза в 1.5-2 медленнее оптимизированной).
— В режиме Ahead-of-Time
компиляции daScript
обгоняет nim
(который вообще показывает достаточно хорошие результаты в нормальных бенчмарках с другими языками).
Заметки
1 - (2009) Python in imaginary world
2 - (2015) Scala in imaginary world
3 - (2021) Nim in imaginary world
Исходники
https://github.com/spiiin/james_bond_jr_problem
Подготовка
Сборка автономного интерпретатора - проект daScript
(можно в cmake поотключать дополнительные библиотеки типа glfw
, ненужные для интерпретатора)option(DAS_XXX_MODULE_DISABLED "Disable any unneeded modules" OFF)
Также не забывать собрать Release-версию. Теперь можно запускать скрипты из командной строки:daScript.exe james_bond_jr.das
Решение
Отличия в синтаксис от Python
:
- Отсутствует присваивание кортежей
Из-за чего нельзя написать сдвиг в массиве как в python
:n[0+plus],n[1+plus],n[2+plus],n[3+plus] = n[3+plus],n[0+plus],n[1+plus],n[2+plus]
и приходится писать отдельную функцию сдвига
- Нельзя сравнить два массива с помощью оператора проверки равенства
Что логично из-за неопределенности поведения такого оператора (сравнивать ли содержимое или указатели). Из-за этого используется самописная функция same
:
- Поддержка именованных именованных кортежей без необходимости использовать отдельный класс
Python
позволяет использовать именованные кортежи вместо обычных там, где не хочется заводить структуру. В daScript
возможность именовать поля кортежа встроена в язык:
- Ошибки вывода типа в генериках иногда напоминают вывод ошибок в шаблонов C++
https://github.com/GaijinEntertainment/daScript/issues/309
- Нет встроенного аналога list из Python и DoubleLinkedList из Nim
Вместо этого кортежи хранятся в классе array
, представляющем собой динамический массив. Для того, чтобы избежать лишнего копирования данных при сортировке, память под кортежи выделяется на стеке:
Также можно отметить, что объекты на хипе выделяются в соседних областях памяти, аллокатор контекста по умолчанию выделяет память из предвыделенного линейного блока:
Так что, в теории, разница в скорости в выделении объектов на стеке и в куче для daScript
должна быть небольшой, и хранение узлов в массиве должно дать даже небольшой прирост скорости из-за локальности хранения узлов в памяти, по отношению к способу хранения в списке.
За исключением перечисленных отличий, код “переведён” построчно с Python версии (с “бонусной” проверкой ошибок типизации интерпретатором). В такой форме при интерпретации он уже работает лишь чуть медленнее скомпилированной версии на Nim
.
Ahead-of-Time компиляция
daScript
можно настроить, чтобы вместо интерпретации он генерировал C++-код, выполняющий те же действия. В репозитория проекта есть пример настройки cmake
для автоматической генерации AoT-версии кода.
Можно проделать этот этап вручную:daScript.exe -aot james_bond_jr.das james_bond_jr.das.cpp
Полученный C++ файл можно скомпилировать (можно просто добавить его в один из туториалов), и теперь при выполнении скрипта james_bond_jr.das
, вместо интерпретации, будут выполнены скомпилированные версии функций. В таком режиме скрипт обгоняет разогнанную nim
версию решения. Выводы в начале.
Ещё быстрее!
Пара оптимизаций, чтобы сделать программу ешё быстрее.
[[unsafe_deref]]
аннотация для функций, которая “инлайнит” обращения по указателям.
Код из ast_simulate:SimNode * ExprPtr2Ref::simulate (Context & context) const {
if ( unsafeDeref ) {
//симуляция выполнения ноды
return subexpr->simulate(context);
} else {
//создание ноды для более поздней симуляции
return context.code->makeNode<SimNode_Ptr2Ref>(at,subexpr->simulate(context));
}
}
Векторизация!
В daScript есть встроенные векторные типы int4 и float4, и описание поля логичнее переделать на их использование:
Тогда горизонтальные сдвиги можно описать так:
Что 1) короче 2) очень быстро
Можно измерить скорость выполнения обычной и оптимизированной версии встроенным профайлером:profile(20, "JamesBondUsual") <|
for i in range(100)
var dif <- extract(search())
Получился прирост скорости ещё на 25% ( 0.4 -> 0.3 миллисекунд за 100 запусков).
Код быстрой версии: