В современном мире программирования динамические языки, такие как Python и JavaScript, занимают лидирующие позиции, во многом благодаря своей простоте и гибкости. Они позволяют писать выразительный и незамысловатый код, не обременённый сложными ограничениями типов, что делает их особенно привлекательными для большинства разработчиков и исследователей в области искусственного интеллекта. Однако, несмотря на все преимущества, динамические языки традиционно сталкиваются с серьёзными проблемами в организации эффективной параллельной обработки данных. Это связано с тем, что современные интерпретируемые языки зачастую не могут полноценно использовать всю мощь многоядерных процессоров, работая преимущественно в однопоточном режиме, что ограничивает их производительность при решении ресурсоёмких задач. Для преодоления этих ограничений рассматривается множество архитектурных подходов, одним из которых является акторная модель параллельности.
Суть этой модели заключается в представлении программных единиц – акторов – как изолированных процессов с собственной памятью, которые взаимодействуют друг с другом исключительно посредством обмена сообщениями. Такой подход исключает необходимость сложных механизмов синхронизации и блокировок, которые зачастую становятся источником ошибок и снижают масштабируемость параллельных приложений. Реализация акторной параллельности безусловно связана с вызовами. Основным из них является необходимость копирования данных при передаче сообщений между акторами, что традиционно воспринимается как достаточно дорогая операция в плане производительности и расхода памяти. Однако современные аппаратные возможности предоставляют оптимизации, которые снижают влияние этих издержек.
Быстрое копирование памяти на современных ЦПУ и возможность использования неизменяемых данных позволяют минимизировать необходимость постоянного дублирования информации. Кроме того, механизм передачи владения объектом между акторами без его копирования становится серьёзным подспорьем для повышения эффективности. В качестве эксперимента и образовательного проекта был создан язык Plush – простой, динамически типизированный язык с интерпретатором на основе стековой виртуальной машины. Plush позиционируется как простой и в то же время функциональный язык, вдохновленный концепциями Lox и JavaScript, но с интегрированной поддержкой акторной параллельности. Особое внимание в его разработке уделялось эффективности обмена сообщениями между акторами и сохранению простоты использования, что делает язык интересной платформой для экспериментов в области параллельного программирования.
Одним из важных факторов, повлиявших на архитектурные решения в Plush, стал отказ от прототипного наследования, характерного для JavaScript, в пользу классического объектно-ориентированного наследования. Прототипное наследование, хотя и гибкое, создаёт сложности при копировании объектов между акторами, особенно в вопросах правильного сохранения иерархии классов, что влияет на корректность операторов типа instanceof. Классическое наследование решает эту проблему, позволяя актору иметь локальную копию иерархии классов, при этом обеспечивая согласованность типов по обе стороны обмена сообщениями. Plush реализует гибкую систему обмена сообщениями, в которой можно передавать произвольные объекты, включая лямбда-выражения и замыкания. При этом происходит структурное копирование объектов и всех связанных с ними данных, что требует внимательности при проектировании взаимодействующих компонентов.
При создании нового актора происходит копирование всех глобальных переменных, что напоминает модель форка процесса, но при этом механизм копирования в Plush достаточно быстрый за счет внутренних оптимизаций виртуальной машины. Внутренне Plush использует уникальную систему аллокации памяти для каждого актора и отдельный пул для почтового ящика сообщений. Такая конфигурация гарантирует локальную и бесперебойную работу каждого актора без необходимости в блокировках на время работы. При отправке сообщения задействуется только защита на уровне блокировки почтового ящика принимающего актора, что минимизирует взаимные задержки. Для оценки производительности системы обмена сообщениями была проведена серия тестов, включающих микротесты типа ping-pong, где объекты пересылаются туда и обратно между акторами с фиксацией времени и нагрузки процессора.
Результаты впечатляют: на современных машинах можно добиться порядка полумиллиона пересылок в течение всего нескольких секунд, что ставит Plush в ряд языков с сопоставимой производительностью, несмотря на интерпретируемый характер и отсутствие на данный момент оптимизаций и компиляции в машинный код. Кроме теоретических экспериментов, Plush был опробован на практике при реализации параллельного рейтрейсера – программы для трассировки лучей, применяемой для генерации высококачественных изображений с реалистичным освещением. Это классическая задача с высоким уровнем вычислительной нагрузки и естественной возможностью распараллеливания по сегментам изображения. Plush получил инструменты под работу с графическим выводом и передачей байтовых массивов для работы с изображениями. Вдохновившись возможностями генерации кода на основе языковых моделей искусственного интеллекта, разработчик Plush использовал LLM-помощников для ускорения создания рейтрейсера и дальнейшей его параллелизации с помощью акторной модели.
Несмотря на то, что языковая модель не была обучена конкретно на Plush-коде, она достаточно быстро поняла синтаксис и семантику языка, а также предложила разумные изменения для распараллеливания задачи. Это подтверждает перспективность совместного использования новых языков с современными инструментами на базе ИИ для быстрой и качественной разработки. На высокопроизводительном десктопе с 16-ядерным процессором Ryzen 7950x параллельная версия рейтрейсера Plush показала ускорение более чем в 15 раз по сравнению с однопоточным режимом. Это впечатляющий результат, учитывая, что без параллельности языки с интерпретатором обычно значительно уступают нативному коду на С++. Важным моментом является то, что даже с учетом затрат на копирование данных между акторами, производительность в реальных задачах незначительно снижается, поскольку основное время тратится на вычислительные операции, которые прекрасно раскладываются по ядрам.
Отсутствие в Plush на данный момент сборщика мусора ограничивает длительность бесперебойной работы длительных вычислений и анимаций, однако перспектива внедрения независимого GC для каждого актора выглядит обещающе. Такой подход позволит уменьшить задержки, связанные с синхронизацией памяти между потоками, сохраняя при этом эффективность и безопасность работы с данными. Глядя вперёд, разработчик планирует улучшать Plush, повышая производительность интерпретатора путём внедрения оптимизаций, а также расширяя функциональность, добавляя аудио поддержку через SDL2 и улучшая разработку примеров и учебных материалов. Проект открыт для участия сообщества и готов принимать вклад в виде тестов, примеров и улучшений. В более широком контексте Plush демонстрирует, что несмотря на мнение некоторых скептиков о застое в развитии новых языков и невозможности конкурировать с уже существующими, инновации, особенно связанные с параллелизмом и использованием современных вычислительных моделей, все еще имеют потенциал для создания новых и интересных инструментов.
Использование известных синтаксических и семантических шаблонов делает язык доступным для новых пользователей, а уникальные решения в области параллельности и управления памятью открывают широкие возможности для приложений в будущем. Таким образом, Plush даёт нам ценный пример того, каким образом можно сочетать традиционные и современные концепции для создания языка, который не только обучает и экспериментирует, но и решает реальные практические задачи с довольно высокой эффективностью. Это свидетельство того, что акторная параллельность имеет будущее в динамических языках и способна улучшить опыт разработки параллельных систем, сделав его более доступным и безопасным для широкого круга программистов.