При разработке очередной платформы перед командой АТОЛ встал вопрос выбора языка программирования/стека технологий/железа/фреймворка для создания решений. Железо было выбрано на базе относительно недорогой Linux-платформы STM32MP153/512MB DDR3/8GB eMMC. Эта платформа имеет на несколько порядков больше ресурсов, чем используемые в нашей основной массе решений LPC1768/LPC1778/LPC4078/STM32F207. 100% наработок кода компании для устройств были написаны на C/C++, однако прогресс не стоит на месте, и периодически необходимо актуализировать инструменты и технологии разработки, особенно с учетом новых аппаратных возможностей. Из статьи станет ясно, как мы дошли до жизни такой и почему выбрали Golang для создания очередного набора решений.
Выбор стека технологий важен для всех компаний, которые занимаются разработкой железа и перерастают крошечные embedded контроллеры на Cortex M0/M3/M4/M7. Обычно команды при переходе на новую платформу выбирают одно из двух решений: стараются сделать новую версию системы на новом железе/технологиях/архитектуре, превращая решение в нестабильный долгострой, или наоборот — вносят минимальное количество изменений, но иногда вместо совокупности положительных черт разных подходов получают совокупность отрицательных.
В статье исследованы особенности различных языков программирования/технологий (Java, Python, C/C++, Rust, Golang), их плюсы и минусы, сформулированы критерии выбора и представлен выбор команды АТОЛ.
Для анализа использован метод SWOT-анализа. В качестве источников данных — информация сайтов фреймворков. Помимо этого, косвенная информация о боли и страданиях разработчиков получена на Stackoverflow, и часть субъективных выводов сделана на основе моего экспертного мнения за более чем 30-летний опыт программирования.
Почему к C/C++ решили добавить язык высокого уровня?
Итак, мы переехали на новое «железо», теперь у нас есть Linux, ресурсов на три порядка больше и целая куча легаси-кода на C/C++, в которых можно напилить нужные уровни абстракции/упаковать в приложение/библиотеку и начать жить по-новому.
Однако у такого подхода есть недостатки против применения языков высокого уровня:
Высокая трудоемкость при реализации новой функциональности, особенно если нужны высокоуровневые сущности (веб-сервер, работа с «хитрыми» базами данных, реализация брокера сообщений и прочее). Теоретически это решается созданием зависимости от third-party библиотек и подключением монструозных библиотек, но в любом случае этот код тяжелее читать/сопровождать/писать/отлаживать/профилировать по сравнению с языками высокого уровня.
Разработка и стабилизация функциональности на C/C++ занимает в разы больше времени, чем аналогичные действия на языках высокого уровня (Python, Golang, Java, C# и прочие), так как с указателями существует много взаимодополняющих способов отстрелить себе ноги, типа memory corrupt/memory leak/null pointer operations и прочих «радостей», которые можно улучшить при использовании новых стандартов/умных указателей, но не избавиться от них совсем.
Порог вхождения новых разработчиков выше, с учетом наличия различных стандартов С++, использования множественного наследования, шаблонов, сторонних библиотек boost/qt и прочих, чем для приложения с аналогичной функциональностью на языках высокого уровня (при создании разумной архитектуры).
Получается заметно больше кода, особенно при создании кроссплатформенных решений.
При всех указанных минусах, отказ от C/C++ для нас невозможен по двум причинам:
большое количество legacy-кода, наработанного за 20 лет существования компании;
мы разработчики оборудования, поэтому C/C++ в любом случае останется при разработке библиотек низкого уровня, написании модулей ядра, модификации низкоуровневых библиотек Linux/Android и прочих ОС, которые могут быть натянуты на новое железо.
Соответственно, нам нужно было выбрать язык/технологию, который дополнит C/C++.
Почему не С#, Ruby, JavaScript, TypeScript, Dart или Julia?
Ruby, JavaScript, TypeScript, Dart, Julia и прочие хайповые языки/технологии, имеющие по индексам TIOBE рейтинг менее 1%. Даже не спрашивайте, почему не они. Будем считать, что они просто не подходят на роль кроссплатформенного языка общего назначения под Embedded Linux/desktop/cloud с целью создания различных системных и веб-сервисов:) На чем создавать кроссплатформенные GUI-приложения — это отдельная большая история, которая выходит за рамки данной статьи.
C#. Он мне нравится как язык/фреймворк для создания Windows-приложений. Я писал на нем лет 10 «боевые» приложения, и до сих пор «грешу», если нужно быстро накидать GUI-приложение «для себя». Но у него есть, на мой взгляд, несколько фатальных недостатков:
Фактически он привязывает разработчика к среде разработки Microsoft Visual Studio (если хоть раз попробовал, то слезть почти невозможно), сборочные машины будут «виндовые», что сразу накладывает особенности/ограничения на ci/devops, который в других случаях красиво контейнеризируется в «легонькие» Linux-докеры.
Имеется сильная зависимость от .NET Framework, а он за последние годы стал сильно «пухленький» по сравнению с первыми версиями 1.1/2.0, которые умещались в какие-то жалкие 13-20 МБ. Это сильно осложняет его применение на устройствах с ограниченными ресурсами.
Возможно, какой-нибудь Universal Windows Platform (UWP) впоследствии вырастет до полноценного кроссплатформенного решения Embedded Linux, десктоп (Linux/Windows/MacOS) и облачных сервисов, но я пока в production такого не видел. Кроме того, поработав длительное время и создавая многопоточные кроссплатформенные сервисы Golang и C#, я однозначно делаю выбор в сторону Golang. Таким образом, C# отсеяли.
Наши альтернативы: Rust, Python, Golang, Java
Целевой ОС для разработки будем считать Linux, однако впоследствии при установке более мощного железа (STM32MP157, NXP серии i.MX и прочие) возможно появление Android, поэтому код должен собираться и туда. Огромным плюсом я считаю возможность собираться под десктоп (для создания эмуляторов устройств и переноса общей логики на выделенную машину в случае работы нескольких одинаковых устройств в сети), а также под облачные сервисы (при отсутствии инфраструктуры у юзера и выносе части логики на нашу или арендуемую клиентом облачную платформу).
В качестве языков высокого уровня будем рассматривать следующие альтернативы: Rust, Python, Golang, Java.
Есть красивые глубокие исследования по low-level производительности Rust, Python, Golang, Java, например, по микросервисам. Есть показательный benchmark разработанного веб-сервиса с задержкой (Latency) в миллисекундах на шкале слева при доверительной вероятности 99% и общей пропускной способности в запросах в секунду (RPS) на одинаковом железе.
Python
По большому счету, Python имеет одни из самых высоких задержек при создании системных сервисов и самую низкую производительность при создании pure python кода. То есть самые ответственные операции все равно придется писать на C/C++. Но я не считаю язык Python языком общего назначения, на мой взгляд, он хорош в боевом применении, только если надо писать облачные сервисы (web, AI), заниматься внутренней автоматизацией или создавать open-source код, хотя и имеет крайне низкий порог вхождения.
Если отойти от указанных применений влево-вправо, то начинаются разного рода приключения:
Важно, но не смертельно — в Python элементарно набиваются зависимости, которые быстро раздувают приложение в 50-100 МБ, если пытаться создать распространяемое кроссплатформенное решение.
Фатально — создание платного ПО, распространяемого по подписке или лицензиям, требует неприемлемых затрат на защиту кода, а иногда выдвигают доп. требования аппаратной защиты (USB-ключи, etc.). Нормальных сторонних обфускаторов тоже нет, а мы — коммерческая компания.
Фатально — при попытке создавать код системных приложений под Embedded Linux получаем жутко непредсказуемое потребление ОЗУ. Хочу, чтобы можно было нормально инкапсулировать логику в отдельные приложения, а не коверкать архитектуру из-за особенностей технологии, отказывая, например, в доступе к БД всему, кроме одного микросервиса, через который нужно пускать в БД остальные, так как работа с БД — это плюс пара десятков МБ ОЗУ, и для пары десятков микросервисов в Embedded Linux потеряем половину ОЗУ.
Можно поразвлекаться с PyPy, и грести его особенности потом в боевых применениях (так как для железа будет PyPy, а в облаке будет Python, и поведение может различаться). Кроме того, в нескольких предыдущих проектах мне неоднократно приходилось бороться с GIL, которого нет в других языках (но когда-нибудь это уже не будет проблемой). В итоге Python тоже отсеяли.
Java
С Java ситуация чуть лучше с точки зрения производительности, но не задержек. С первыми двумя минусами Python у Java получше, из-за JRE (входит в minimum requirements для платформы) и обфускаторов коммерческих решений — вагон.
Но один фатальный момент сохраняется:
Высокие требования к Flash, ОЗУ, а также вычислительной мощности из-за наличия «жирненького» JRE. Если бы у нас был только Android, то ситуация была бы немного лучше, но у нас cross-platform, да еще имеется Embedded Linux в скромной конфигурации.
Однако ответственные вещи можно переписывать на C/C++ или использовать низкоуровневые оптимизированные библиотеки, а в некоторых тестах и встроенные средства в Java ведут себя неплохо. Но подход постоянно подкладывать низкоуровневый код лишает преимуществ высокоуровневого языка, и фатальные проблемы никуда не деваются.
Rust vs Golang
Rust. В сети периодически возникают идеи отказа от C/C++ в пользу Rust (ссылка 1, ссылка 2), но мы все прекрасно понимаем, что поделить бассейн на «писающих» и «не писающих» не получится, так как весь код вокруг остается на C/C++ и его придется сопровождать/развивать AS-IS, поэтому применение C/C++ для нас неизбежно. Однако можно добавить один высокоуровневый язык для понижения сложности сопровождения системы, снижения стоимости масштабирования/новой функциональности. Пару языков добавлять не хочется, чтобы не разводить зоопарк и не повышать сильно порог вхождения в разработку.
В итоге основная битва развернулась между Rust (рассматриваем как «язык высокого уровня») и Golang. Глядя на код, на одном и другом языке после C/C++ ничего смертельного не видно. Глядя на плюсы и минусы языков/технологий, я предвзято склонялся к Golang. Поэтому сделал SWOT-анализ для случая, если выберем Golang вместо Rust, чтобы себя проверить. Общие черты типа «memory safety», «быстрый», «компилируемый» в таблице не отражены, показаны только различия:
SWOT-анализ, если выбрать Golang вместо Rust | |
Сильное | Слабое |
• Меньше вероятность проблем при кроссплатформенной разработке из-за более широкой популярности (в два раза по индексу TIOBE) • Отличная концепция многопоточности (goroutines) и обмена сообщений (channel) • Много встроенных полезных инструментов (тестирование, управление пакетами), как и в Rust, но дополнительно есть статический анализ, design-time анализ в средах разработки и профайлер • Намного более быстрая сборка (кто часами ждал сборки на C/C++ оценит:) ) • В Golang синтаксис близок к C, и порог вхождения ниже, чем в Rust (steep learning curve линк 1, линк 2), что позволяет легче писать и сопровождать код, но из-за этого нет «zero cost abstractions», которые есть у Rust | • Не получим такого хорошего детерминированного поведения кода, как у Rust • В большинстве тестов Golang проигрывает Rust, хоть и не так сильно, как другие высокоуровневые языки • Golang использует «garbage collector» (больше потребление ОЗУ) против модели «lifetimes» у Rust, то есть это не язык системного программиста • Golang внутри имеет промежуточные слои абстракции |
Перспективы | Подводные камни |
• Верим больше в технологии развиваемые гигантом Google, что не «помрет», как другие, так как активно используется в массовых решениях (Google Chrome, Google Earth, YouTube, Kubernetes, Docker, GitHub) • Time to market фич/продуктов на Golang будет ниже, чем у Rust | • Возможно, будем упираться в недетерминированное поведение, повышенный расход ОЗУ/CPU, и это чаще придется переписывать на C/C++ или использовать сторонние оптимизированные библиотеки |
Если бы надо было выбрать только один язык, то, возможно, я бы и склонился к Rust, но так как надо выбрать язык/технологию, который дополнит C/C++, то слабые стороны Golang теряют вес. Если использовать последние стандарты C++ и умные указатели, то также можно улучшить ситуацию и понизить необходимость в Rust. Кроме того, для компании сейчас важно минимизировать время time to market: вместо стабилизации нового продукта месяцами хочется стабилизировать его неделями. Поэтому выбор пал на Golang.
При экспериментах с Golang на конкретном выбранном микроконтроллере STM32MP1 стало ясно, что у нас нет удаленной отладки (Delve remote debugging) из-за 32-bit arm архитектуры. Все новые контроллеры уже 64-bit, поэтому Google, видимо, не особо «напрягается», хотя уже есть x86 и 32-bit mips. Видимо, придется эту задачу решать первой, когда будем подходить к промышленному применению.
Понятно, что впереди маячит Go 2, где наконец-то порешают долгожданный вопрос с обработкой ошибок, и очевидно, что мы столкнемся с местами, где Rust подошел бы больше, чем Golang, и их придется написать на C/C++. Но, как гласит старая русская пословица, «багов бояться — код не писать».
Спасибо, что дочитали до конца :)