🗣 Java и C# устарели в эпоху Docker
Языки, работающие на виртуалках, необходимы для упрощения развертывания на любой платформе. Но эта способность более не важна, когда ваше ПО работает в контейнере.
Перевод публикуется с сокращениями, автор оригинальной статьи Erik Engheim.
Java и C# находятся в аналогичном положении. Чтобы строить корпоративные системы в облаке, сегодня не нужно создавать ориентированный на виртуальную машину системный язык. До появления контейнеров и облаков имело смысл развернуть ПО на любом сервере в виде jar-файла. С помощью Docker и других контейнерных технологий мы продвинулись еще на один шаг вперед. Вместо Java Virtual Machine у нас есть Linux Virtual Machine, которую можно создать с помощью контейнера Docker и работать где угодно.
В чем преимущество? У вас есть целая среда в ОС с файловыми системами, службами и т. д.
Подход Java и C# означал создание общеязыковой среды выполнения, где вы могли бы применять специальные языковые варианты, вроде Jyton (JVM) или Iron Python (.NET), создающие код для этой конкретной виртуальной машины. И Java, и C# используют JIT-компиляторы. То же самое относится и к промежуточному языку .NET (IL). Аппаратное обеспечение, которое непосредственно понимает байт-код Java или .NET IL, не существует в реальном мире. Это воображаемое оборудование, воплощенное в жизнь с помощью программного обеспечения.
Зачем создавать альтернативные версии каждого языка для запуска на одной виртуальной машине, когда вы можете просто запустить их все в одном контейнере Docker?
Разница в подходе Java и C#
Что говорят в Microsoft на эту тему:
Приложения Windows 8.1, настольный софт Windows и нацеленные на .NET фреймворки пишутся на определенном языке программирования и компилируются в промежуточный язык (IL). В рантайме JIT-компилятор отвечает за компиляцию IL в машинный код для локальной машины непосредственно перед первым выполнением.
Другими словами, программы на Java и C# распространяются одинаково. Оба поставляются в виде байт-кода или промежуточного языка (IL). Java может использовать сторонний код в jar-файлах, C# может использовать сторонний код в сборках.
The Native Image Generator (NGEN)
Ключевое отличие Java заключается в том, что C# обычно использует так называемый Native Image Generator (NGEN). Он все еще подразумевает стратегию компиляции just-in-time, а еще использует тот факт, что JIT создает кучу нативного кода в памяти. Это представление в памяти всего скомпилированного кода обычно называется образом.
Поскольку автор не является экспертом в C#, будем использовать Julia в качестве примера. Она предназначена для JIT-компиляции. Чтобы создать образ стандартной библиотеки, всегда плодится много кода для осуществления связи всех важных областей. Это приводит к тому, что большинство функций компилируются в машинный код. По сути тут создается кэш. У вас нет никакой гарантии, что удастся управлять всем с JIT, поэтому вам все еще нужна среда выполнения и JIT-компилятор для запуска кода. Вы просто избавляете себя от повторной компиляции часто используемых функций.
То же самое относится и к .NET NGEN – он не компилирует все в машинный код и вам также нужна среда выполнения и JIT-компилятор для взаимодействия со сборками .NET (эквивалент библиотек DLL).
.NET Native
Такой подход явно ведет C# в направлении, отличном от задуманного разработчиками. Например, вы не сможете:
- выполнять вызовы из собственного двоичного .NET-файла в другие сборки .NET;
- использовать рефлексию для вызова произвольного кода в сторонних сборках.
Вы вынуждены выбирать – либо запускаете свои C#-приложения через JIT и получаете доступ к любому количеству сторонних библиотек (в виде сборок), либо предварительно компилируете, и оно может использовать только код из бинарника.
Из-за чего возникает ограничение?
Для языков .NET не определен бинарный интерфейс нативного кода. Нет никакого способа вызвать скомпилированную в нем сборку, т. к. заранее неизвестен тип соглашения о вызове. В отличие от Swift 5, который был разработан для нативной компиляции с нуля и имеет хорошо определенный стабильный бинарный интерфейс приложения (ABI).
Собственный компилятор будет анализировать, какой код в сторонних сборках (библиотеках) вызывает ваше приложение C#, а затем встраивать его в бинарник. Если вы вызываете код в сторонних библиотеках через рефлексию, то компилятор не сможет определить вызываемый код, поскольку это будет решением среды исполнения.
Облако решает проблему развертывания
Простое развертывание, которое должен был обеспечить байт-код, может быть достигнуто другими способами. Например, Swift отправляет свой байт-код в AppStore, а не на ваш компьютер. Облако будет компилировать его для каждой поддерживаемой платформы. Это означает, что вам не нужен какой-либо интерпретатор байтового кода или компилятор, установленный компьютере и поддерживаемый в актуальном состоянии. Это не то же самое, что контейнерное решение, но так можно избавиться от необходимости распространять байт-код напрямую пользователям.
Больше нет смысла изучать Java или C#?
Эти языки все еще имеют значение, из-за огромных экосистем и миллионов нуждающихся в поддержке строк кода. Новые системы будут по-прежнему строиться с использованием Java и C#, потому что они пользуются преимуществом за счет наличия большого количества библиотек.
Go – это Java для Docker-эпохи
Когда приложение работает в контейнере Docker, можно свободно компилировать его в машинный код, что и делает Go. Это дает несколько преимуществ:
- Контейнеры Docker могут быть меньше, поскольку не нужно устанавливать большую инфраструктуру виртуальных машин, JIT-компилятор, среду выполнения и т. д.;
- Простое развертывание – для использования без контейнера гораздо проще распространять созданный Go бинарник. Можно загрузить его на ПК и запустить без необходимости установки JVM или Common Language Runtime;
- Кросс-компиляция позволяет легко создавать бинарные версии для различных платформ.
Java, C# и другие управляемые среды, которые были популярны в 90-е годы, теперь просто представляют собой ненужный дополнительный слой абстракции. Контейнеры создают прослойку между вашей средой и реальным оборудованием. Зачем создавать еще одну? Она поглощает больше ресурсов и замедляет работу системы.
Являются ли JIT-компиляторы устаревшими?
И Java, и C# используют JIT-компиляцию, в отличие от Go и Rust. Можно подумать, что такая компиляция – путь в никуда. Все не так просто: Lua, JavaScript, Julia, Pharo (производные от Smalltalk), R, Matlab, LISP и Python – это языки, которые либо уже используют JIT, либо начали на него переходить. Они не являются системными языками программирования и они динамически типизированные.
Из различных бенчмарков можно увидеть, что несмотря на многолетнее лидерство, Java обычно уступает Go почти по всем показателям. Go будет выполнять больше вычислений, обрабатывать больше запросов, потреблять меньше памяти и давать меньшую задержку. Однако для динамических языков JIT дает огромное преимущество, а выполнять ahead-of-time компиляцию для динамического языка непрактично и неудобно.
Важно ли это в реальном мире?
Java и C# устарели в теории, но бесчисленные рабочие места требуют этих технологий, так имеет ли это наблюдение какое-либо практическое значение для реального мира? В краткосрочной перспективе – нет. Мы увидим постепенное снижение популярности, люди будут мигрировать на другие альтернативные языки.
Эти альтернативы обычно обеспечивают лучшую производительность, более простое развертывание и меньшее использование памяти. Для любого, кто начинает новый проект, это имеет значение. Иногда Java и C# будут единственным естественным выбором, потому что конкурентам не хватает библиотек и инструментов в конкретной области. По мере того, как конкуренты расширят экосистемы и инструментарий, привлекательность Java и C# будет снижаться.
Настольные (GUI) и мобильные приложения
Настольные приложения обычно адаптируются к среде исполнения. Развертывание на любой ОС или оборудовании – меньшая проблема. Например, что бы вы предпочли использовать для разработки под OS X: Java или Swift? Основанная на виртуальной машине Java не дает никаких преимуществ! Вы получаете больший объем расходуемой памяти и длительное время запуска. Помните, что в отличие от серверного ПО, настольное не работает месяцами – оно запускается на несколько часов. В этом случае компиляция Ahead-of-Time дает явные преимущества.
Можно сравнить iOS и Android. Отклик софта на iPhone всегда быстрее даже на слабом железе, поскольку Swift не нуждается в JIT-компиляции или сборке мусора. Это также дает преимущества в сфере безопасности. В iOS вы можете использовать криптоподпись страницы в памяти, чтобы убедиться в отсутствии ее модификации. Вы не можете сделать это с помощью JIT-кода, потому что заранее не знаете, как он будет выглядеть.
Что насчет Windows? C# популярен для создания GUI. В свое время он был долгожданным событием после Win32 C API и Microsoft Foundation Classes (MFC), но это было связано больше с дизайном библиотеки чем с языком. Кросс-платформенный GUI Qt tookit обеспечивал во многом те же преимущества с точки зрения простоты программирования. Qt написан на C++, а не на C# или Java. Если вы пишете сложные GUI-приложения сегодня, скорее всего вы предпочтете Qt любому решению C#.
Как Java и C# попытаются нанести ответный удар
Хорошо зарекомендовавшие себя технологии редко сдаются без боя. Скорее всего мы увидим возросшие усилия по превращению Java и C# в среды для компиляции в машинный код. .NET Native и GraalVM native Image – примеры этого. Примерно так Python пытается противостоять Julia, создавая собственные JIT-решения. Это работает, но огромное наследие было построено на совершенно других принципах. Функции вроде рефлексии могут перестать работать.
В чем самая большая проблема? Быть незрелым или иметь тяжкое наследие? Общество любит делать ставки на новичков, но устоявшаяся индустрия часто удивляет. Oracle и Microsoft вполне могут в конечном итоге одуматься, переосмыслив свои языки и инструменты. Время покажет.
Заключение
Docker – контейнер для облачных решений и продукт развития облачного горизонтального масштабирования и микросервисов. C# и Java были созданы для мира больших выделенных серверов. При масштабировании на большом количестве машин требуется простое развертывание, репликация конфигурации и настроек. Все это дают контейнеры. Если вы настраиваете одну машину, можно использовать что угодно.
Экосистема Linux страдает от необходимости создавать пакеты приложений для каждого дистрибутива, поскольку они используют разные системы управления установленным ПО, версии зависимых программ и т. д. С помощью контейнерных технологий, вроде Flatpak и Snap, вы можете преодолеть эти различия и получить возможность установки и запуска в один клик на любом дистрибутиве.
Это касается не только Linux. Приложения из AppStore также основаны на контейнерах. Здесь мы видим полную альтернативу подходу Java и C#. Облачная служба определяет оборудование, а пользователи загружает специально разработанные под его машину приложения. Если такой механизм распределения слишком сложен, можно создать «жирный» универсальный бинарник.
Любое решение устраняет необходимость установки JIT-компилятора, сред выполнения и фреймворков для Java или C#. Поставляемый в контейнерах софт, облегчает распространение и развертывание.
Дополнительные материалы:
- 10 самых популярных алгоритмов сортировки на C#
- Как запустить веб-приложение на Nginx в Docker
- Покажем, как использовать docker-compose для Python и Jupyter
- Исчерпывающий видеокурс: структуры данных C#
- Лучшие актуальные шпаргалки по C# на все случаи жизни