Сравнение кодовых баз

Всем привет! Как я и обещал в первой части этой серии сообщений в блоге, я рад приветствовать вас во второй части нашего путешествия, где я пытаюсь найти правду, сравнивая Kotlin/Native с C++, Freepascal и Python через его производительность с openGL. графика и кодирование удовольствие.

Содержание

  1. Простая математика с нативными приложениями, или Кто сегодня такой умный?
  2. Графический тест с SDL2 или Через Вселенную с Norton Commander. — вы здесь —

Мотивация

Этот пост Вячеслава Архипова вдохновил меня начать исследование. Если вы старый гик, то должны помнить, что старые ЭЛТ-дисплеи для ПК имели эффект выгорания.

Вики:Прожиг — это когда изображения физически вжигаются в экран ЭЛТ; это происходит из-за деградации люминофоров из-за продолжительной электронной бомбардировки люминофоров и происходит, когда фиксированное изображение или логотип слишком долго остаются на экране, что приводит к тому, что оно появляется как фантомное изображение или, в тяжелых случаях, также при выключенном ЭЛТ. Чтобы противостоять этому, на компьютерах использовались заставки, чтобы минимизировать выгорание. Выгорание характерно не только для ЭЛТ, но и для плазменных и OLED-дисплеев.

И одной из самых известных заставок для ПК была Starfield, которую вы должны увидеть под заголовком этого поста. Представьте, что вы командир Джеймсон внутри Cobra MK3 и путешествуете по Вселенной в поисках кораблей Таргоидов. Звезды носятся, а ты быстрее молнии в сто раз…

Начиная с Питона

Я немного обновил код из исходного кода, чтобы в заголовке окна отображалось FPS, чтобы использовать другие функции рисования для получения эффекта сглаживания и добавить свойство переменного радиуса для звезды, таким образом звезды получили эффект плавного роста:

Это полный код на Python, но не забудьте установить библиотеку Pygame, если хотите воспроизвести ее на своем компьютере.

Самое сложное — перевести его на другие языки. Сама библиотека Pygame использует графическую библиотеку SDL для инкапсуляции низкоуровневых интерфейсов с помощью openGL (или DirectX в Windows). Все, что нам нужно, это написать собственный xxGame движок для каждого языка… Угу. Пойдем.

Поул-позиция C++

Я думаю, что самый простой способ начать — реализовать Starfield на нативной платформе для SDL, то есть на языке C++, а затем превратить его в варианты Kotlin или Freepascal.

Отказ от ответственности.Поскольку я мало знаком с прямым программированием SDL на C++, я изучил его основы из серии видеороликов Карла Берча на YouTube здесь и рекомендую их как идеальное начало.

Давайте отойдем от кода Python выше и воспользуемся преимуществами языка ++. В C++ есть два типа классов, которые мы можем использовать: сам class и struct. Разница между ними заключается в том, как члены класса объявляются по умолчанию, как частные или как общедоступные члены. В остальном и класс, и структура аналогичны.

Я объявляю struct Star и инкапсулирую его поведение для создания, обновления и возрождения:

У него есть конструктор по умолчанию, который вызывает функцию newStar, и подобное также происходит, когда эта звезда перемещается за пределы экрана. Мы будем использовать массив (вектор в словах C++ STL) из Star объектов вместо глобального списка с безымянными параметрами, как это представлено в Python.

Конечно, мы могли бы использовать статический массив в простом C++ вместо динамического вектора, потому что мы уже знаем его размер, но я предпочитаю делать код с некоторыми возможностями для будущих улучшений. Например, мы можем динамически увеличивать или уменьшать количество звездочек с помощью вектора во время выполнения.

В начале нашей функции main в программе на C++ мы должны подготовить движок SDL к рисованию:

Здесь будут созданы два указателя для окна SDL и контекста рендеринга. Да, мы можем создавать любое количество окон и визуализировать контексты с помощью SDL. Все последующие используемые функции SDL, которые выполняют рисование, должны использовать контекст рендеринга, а некоторым другим функциям для выполнения требуется указатель окна.

Но есть небольшая проблема… В SDL нет функции рисовать закрашенный или очерченный круг из коробки! Благодаря Github я нашел нужный алгоритм за считанные секунды. Этот код использует алгоритм круга Брезенхема и предоставлен нам Gumichan01 по лицензии MIT. Все блестяще — просто! Посмотрите, как просто нарисовать растровый круг:

Итак, теперь мы готовы к основному коду:

Я пропустил некоторые части кода, но вы можете полностью клонировать проект из моего репозитория.

Итак, как видите, я написал очень простой код на C++, и он почти похож на код Python, с той лишь разницей, что используется структура и инкапсулируется некоторое поведение. Этот код должен стать отправной точкой для следующего, в Kotlin. Давайте посмотрим, как мы могли бы улучшить этот код без снижения производительности.

Котлин-стиль

Программирование на простом openGL и SDL представляет собой прочную структурную последовательность функций без каких-либо логических блоков или функциональных областей внутри. Следующий псевдокод буквально показывает, что я имею в виду:

init all;
clear screen; // suppose, 1st frame
draw point;
swap buffers;
clear screen; // 2nd frame
draw lines;
draw triangles;
draw anything else;
swap buffers;
clear screen; // 3rd frame, etc.

Это стандартный поток, который можно запустить в любой программе C/C++. Конечно, вы могли бы использовать логические блоки и организовывать рамки рисования самостоятельно, но фреймворк вас к этому не принуждает.

Kotlin дает возможность создавать код в декларативном стиле с использованием DSL. С помощью DSL вы можете обернуть один логический блок в другой и указать ограничения на его объем или последовательность вызова блоков с четким и лаконичным синтаксисом. Если вы уже знакомы с Ktor или другим фреймворком Kotlin, построенным в стиле DSL, вы меня понимаете. Кстати, известный в мире JVM 2D-игровой движок LibGDX имеет улучшения Kotlin под названием LibKTX, которые позволяют использовать стиль DSL и быстрее разрабатывать игры с понятным, автоматически объясняющим кодом.

Я изучал, как использовать этот фреймворк в серии видеороликов Quillraven Учебник по LibGDX Kotlin, пока не создал свою первую простую мультиплатформенную игру Deter Revolution. И теперь Quillraven строит свой собственный легкий движок ECS (entity component system) под названием Fleks поверх LibGDX, используя Kotlin DSL.

Но, нет слов! Пусть код покажет, что я имею в виду:

SDL Engine {
    init Engine()
    init Environment()
    add Event Listener {
        for quit event -> stop engine and close app
    }
    start Infinite Loop {
        on Each Frame {
            draw magic
            show FPS        
        }
    }
}

Именно так я себе представляю основной код Kotlin варианта Starfield! Вы можете быть удивлены тем, что DSL позволяет преобразовать этот псевдокод в Kotlin буквально один к одному:

Красиво, правда? Это целая основная функция. SDLEngine — это функция с приемником класса Engine. Внутри лямбды (которая является последним параметром функции) экземпляр этого класса доступен как переменная this, и вы можете исключить его в кодовом слове this, если это не так. t создавать помехи любому другому восходящему приемнику.

В функцию SDLEngine я перенес параметры инициализации движка — ширину и высоту окна. addEventListener — это функция внутри области действия движка, которая будет вызываться при каждом событии от движка.

startInfiniteLoop также является функцией внутри движка, которая включает графический рендерер. В моем простом движке эта функция будет работать до закрытия окна. onEachFrame — это функция с приемником указателя SDL Renderer, которая необходима для любой функции рисования SDL. Я объявил renderer закрытым членом класса Engine, и вы не получите его вне функции onEachFrame, поэтому вы не сможете рисовать что-либо вне кадра.

На мой взгляд, такой декларативный стиль позволяет избежать ошибок и вносит больше ясности и простоты. Но для первого…

Как включить библиотеку SDL в Kotlin/Native

Это была действительно сложная часть моего открытия, но мой нынешний объем знаний позволяет пройти этот путь быстрее. Сначала вы должны установить его SDL2 на свой компьютер. Я использовал Ubuntu, и все, что мне нужно, это sudo apt install libsdl2-dev. Установка для других платформ отличается.

Затем вам следует внимательно изучить следующие руководства и статьи из официальной документации Kotlin:

  1. Начните работу с Kotlin/Native в IntelliJ IDEA. В этом руководстве представлены основы создания нативных приложений в Kotlin с использованием IDEA.
  2. Взаимодействие с C. Это самая важная часть! В нем описывается, как включить сторонние динамические библиотеки в ваш проект. С каждой библиотекой много специфических вещей, касающихся только ее. И не пропустите часть Bindings! Это очень важно, если вы планируете использовать типы C++, такие как указатели, перечисления, классы/структуры и обратные вызовы.
  3. Отображение примитивных типов данных из C. Как я еще не сказал, что это тоже очень важная часть?

Итак, с учетом этого туториала, я создал внутри проекта следующие каталоги: project/src/nativeInterop/citerop и поместил в него файл sdl2.def. Файл содержит это:

package = platform.SDL2
headers = SDL2/SDL.h
compilerOpts = -I/usr/include -I/usr/include/x86_64-linux-gnu -I/usr/include/SDL2 -D_POSIX_SOURCE
compilerOpts.linux = -D_REENTRANT
linkerOpts = -L/usr/lib/x86_64-linux-gnu/ -L/usr/lib64 -lSDL2

Затем я отредактировал файл проекта build.gradle.kts, добавив к расширению kotlin/nativeTarget следующее:

compilations.getByName("main") {
    cinterops {
        val sdl2 by creating
    }
}

После этого (и, конечно же, синхронизации Gradle) я нашел новую задачу под названием cinteropSdl2Native в окне инструментов задач Gradle. Эту задачу необходимо запустить до того, как вы попытаетесь использовать любой из заголовков файлов *.def.

Это не финиш! После всего этого пришло время восстановить проект. И теперь мы можем импортировать новый пакет platform.SDL2 (имя определено в файле .def).

SDL Engine в Kotlin DSL

Итак, теперь мы можем превратить DSL в SDL! Это красивая анаграмма. Вот полный код файла с моим движком SDL:

Он делает то же самое, что и его брат на C++, но инкапсулирует его вдали от пользователя [SDL]. Star класс, который я определил, является простым классом, а не классом данных, потому что он позволяет избежать ненужного сгенерированного кода, который полезен для классов данных при их сравнении друг с другом. Это не нужно для моей программы.

Репозиторий с остальным кодом вы можете найти здесь, на моем Github.

Использование Freepascal SDL

Freepascal включает заголовки SDL из коробки, начиная с версии 2.2.2. Но нам нужны заголовки SDL2. Сайт https://www.freepascal-meets-sdl.net/ может нам помочь. Кроме того, это идеальное место для начала изучения SDL2 в Pascal, если вы новичок в этом.

Загрузите заголовки Pascal для SDL2 из этого репозитория Github и поместите их в предпочтительный каталог (я всегда устанавливаю каталог по умолчанию, который Lazarus создал в /home/user/.lazarus).

Тогда, пожалуйста, внимательно прочитайте туториалы с упомянутого сайта и read.me из репозитория, потому что модули взаимодействия Pascal имеют другие имена типов, не похожие на типы C++. Как и подготовка Kotlin c-interop, не так ли?

Ниже приведена преобразованная из C++ функция для рисования кругов в Freepascal:

И это полный код для Freepascal, аналогичный варианту C++:

Конкурс и результаты

Наконец, мы можем начать с нашей начальной идеи. Давайте создадим релиз для каждого языка (знаю, знаю, Python, нельзя) и посмотрим, насколько он быстрый и насколько он маленький.

Это спецификация моего ноутбука, который я использовал для кодирования и тестирования: Acer Aspire 3, 8 ГБ ОЗУ, процессор Intel Core i3–1005G1, диск NVME m.2 240 ГБ, Ubuntu 22.04 x64, OpenGL 4.6, SDL2 и последний версии Intellij IDEA, VS Code, Lazarus и PyCharm, OBS для захвата экрана.

Я измерил размер исполняемого файла, FPS с 200 звездами, а также используемую память и загрузку процессора. С помощью OBS я заснял, как запускались все программы, но OBS съел почти половину всех ресурсов, поэтому FPS на видео был меньше, чем в чистых тестах:

Я получил следующие чистые результаты с 200 звездами Starfield:

Результат является доказательством всех доказательств. Но подумайте, я не зайду так далеко, если скажу, что небольшая разница между C++, Freepascal и Kotlin чисто статистическая.

Говоря о Python, я думаю, что у PyGame есть своя стратегия оптимизации, и FPS меньше 500 не повод для трагедии. Кроме того, я не знаю, какой именно алгоритм круга использует PyGame, но он немного отличается от нашего пользовательского алгоритма для других.

Но давайте встряхнем машину и отойдем от детских вещей. Кому нужны 200 звезд? Пусть нарисует 5000!

Во время захвата экрана, когда OBS снова съедал половину ресурсов (и мои колени горели от тепла ноутбука), FPS был практически одинаковым у всех наших участников! И это было удивительно для меня. Но еще больше меня удивили результаты чистого теста:

Вы видите это? Чистая программа C++, чистая программа Freepascal и чистая программа Kotlin/Native потребляют буквально одни и те же ресурсы и дают буквально одинаковый FPS. Но если прокрутить вверх, то будет видно, что код программы Kotlin сложнее, чем код C++ и Freepascal. Позже для удобства мы написали сложную DSL-логику, но она не влияет на производительность во время выполнения — для нее нужна та же оперативная память, та же нагрузка на ЦП, что и для меньшего низкоуровневого кода. Даже размер исполняемого файла в 3 раза меньше, чем файл Freepascal (без отладочной информации).

Конечно, у Kotlin как у молодого языка нет большого коммерческого графического движка, такого как Unity3D для C# или UnrealEngine для C++, но у него есть прекрасная возможность занять свое место в этом мире. Я считаю, что это только начало большой и длинной истории!

Что касается меня, то у меня есть все доказательства того, что выбор Kotlin был правильным решением последних лет: мобильные Android, back-end, десктопные приложения, 2D- и 3D-игры — все эти дороги открыты для Kotlin.

Спасибо, что дочитали до конца!