Больше чем Java?

в 15:40, , рубрики: C#, ceylon, java, kotlin, qt, scala, производительность, языки программирования

Закончив очередной проект на Java, я попытался разобраться в причинах накопившегося раздражения. Да я люблю Яву и все такое, но… Есть несколько «но», которые досаждают. Приходится писать довольно много шаблонного кода, с генерацией которого вполне может справиться сам компилятор, IDE, конечно, выручает, но это не решение проблемы, а скорее костыль: если что-то изменилось, нужно перегенерить и вычистить и т.д. Проверки на null! Это зубная боль, по-хорошему, нужно делать их всегда дабы не нарваться на «нежданчик» в виде NullPointerException в самый неподходящий момент. Короче говоря, появилось желание посмотреть, что еще появилось в природе и сможет ли это нечто заменить мне Java. Дальше имеет смысл описать участников данного сравнения. Сразу скажу, что не претендую на полноту анализа, к сожалению, у меня было слишком мало времени, чтобы как следует познакомиться с каждым языком.

Обязательные требования к претендентам, которые у меня были:

• Язык общего назначения
• Кроссплатформенность (хотя бы Windows/Linux)
• Стабильность
• Статическая типизация
• Автоматическая уборка памяти
• Поддержка полноценной объектно-ориентированной парадигмы
• Хорошая поддержка в IDE (Eclipse, IDEA или на худой конец NetBeans)
• Безгемморойный доступ к существующим фреймворкам/библиотекам
• Производительность на уровне Java

Помимо этого очень хотелось бы:

• Null pointer безопасность обеспечивающаяся языком на этапе компиляции
• Простые приведения типов без рудиментов из С
• Возможность разумной перегрузки операторов
• Чтобы, наконец-то, были почищены базовые типы. Ну на фига тянуть в новый язык из бородатых времен всякие float/double, short/int/long? Может это и имело смысл во времена i386 процессоров, которые могли иметь математический сопроцессор, а могли и нет. Все эти тонкости могут жить в ядре операционной системы поближе к железу и не высовываться наверх. Базовые сущности: Integer и Float вот и все, что должно быть, детали пусть рулит компилятор.

• Гармоничное сосуществование лямбд
• Общий продуманный и целостный дизайн языка
• Выразительность, поменьше ASCII арта и загадочных конструкций, на которые хочется побрызгать святой водой
• Нормальные строки, в которых символы представлены не суррогатными парами или россыпью байтов, а полноценными Unicode point-ами.

Должен отметить, что в обзор не попал целый пласт языков, которые компилируются в нативные бинари или в C. Возможно, они и замечательные, но попробовав несколько вариантов типа D, Nim, Eiffel, Rust, Go, и еще чего-то я пришел к выводу, что не буду углубляться в эти изыскания. Проверка была очень простая – HelloWord. Бинарь получившийся на выходе обычно измерялся мегабайтами. Т.е. маленькая програмулька, которая печатает в консоль тупую строчку и должна занимать от силы 3-4Кб весит мегабайты! Мелочь скажете вы. Да мелочь, я понимаю, создатели языка впихивают в бинари всю мощь и богатство своего детища, но можно же было как-то оптимизировать это!? Если требуется только печать, зачем запузыривать в бинарь все что есть на свете? Ну вынесите минимально необходимую часть в статическую библиотеку, а все остальное в динамические либы. Если не ошибаюсь, создатели D и Rust позиционируют эти языки пригодными для написания ядра операционной системы. Вы серьезно, ребята? А вот всю эту “красоту”, мегабайты вспомогательного кода языка, с кучей системных вызовов Window/Linux вы тоже предлагаете затянуть в ядро? В общем, этот класс языков не вписался в мое требование «кросплатформенность» и «стабильность» (ну и с «автоматической уборкой памяти» и «объектно-ориентированностью» не всегда все гладко, то интерфейсы забудут добавить, то вместо классов какие-то сомнительные альтернативы предлагают) т.к. они содержат очень жирную прослойку между языком и операционной системой, а качество и стабильность этого кода вызывает много вопросов. Может быть он очень хорош, я не знаю, а если нет? Все-таки JVM и Java SDK проверены годами и прекрасно трудятся на разных ОС. Поэтому все рассматриваемые далее языки в той или иной мере опираются на JVM. Теперь перейдем к списку наших участников, с небольшим описанием, а точнее с моими субъективными впечатлениями, не стоит воспринимать их слишком серьезно.

C# (MSVC2015) — вошел в обзор только потому, что мне довелось написать на нем немалое количество строк, и никаких преимуществ по сравнению с Java я в нем не обнаружил. Скорее наоборот. К тому же с кросплатформенностью тут все не слишком радужно, родная для него платформа Windows, а mono не дает полноценной замены на Linux (см. ниже). Но мне было интересно сравнить его с точки зрения производительности с остальными участниками.

Qt/C++ (5.10) — так же был добавлен исключительно для сравнения производительности.

Ceylon (1.3.3) – этот замечательный язык был создан в 2011 Гавином Кингом, автором известного фреймворка для Java Hibernate и поддерживается сейчас сообществом RedHat. Язык очень красив, продуман, и выразителен. Наверное, так бы выглядела Java если бы развивалась без гнета обратной совместимости. Язык включает в себя все мои вышеперечисленные «хотелки» и даже более того. Я действительно стал поклонником этого языка и потратил больше всего времени на его изучение, к сожалению, информации по нему не очень много, а сообщество не слишком многочисленное и активное. На мой вкус язык почти идеален, но хотелось бы упомянуть и о недостатках. Есть плагины для Eclipse/IDE, они сделаны весьма хорошо, но оптимизация не на высшем уровне, короче говоря, они безбожно тормозят. Запуск тестов (есть специальный фреймворк для тестирования) может занять 7(!) секунд, причем сами тесты выполняются мгновенно, но старт очень неторопливый.

Скорее всего, это связано со вторым недостатком – вшитую в язык систему модульности, нет, я не хочу сказать, что модульность сама по себе это недостаток, я лишь имел ввиду, что встроенные в язык конкретные вещи становятся безальтернативными и это плохо. Так вот в Цейлоне модули имеют свой формат (.car файлы), а основано это все на системе модулей JBoss. Еще создатели языка почему-то отказались от 'protected', что не дает возможность реализовать, например, такой шаблон проектирования как «Шаблонный метод».

Scala (2.12) – такое впечатление, что создатели языка руководствовались, в основном, принципом «а давайте сделаем все ни как у людей». Вместо привычной * сделали _, а зачем? Видимо, «чтобы никто не догадался» (с). Все знают, что массив это обычно [], так давайте сделаем () и пусть им вынесет мозг. А что же тогда будет значить []? А давайте это будут дженерики? Давайте… И так далее. Чтобы вы думали значит a < — b? Я было подумал, ну небось, клонирование, или запихнуть все из коллекции b в коллекцию a, ан нет, это обход коллекции…

А почему? Да потому, что это Scala, потому что они с Тау Кита, кровь у них фиолетовая и мозги работают по-другому, короче они иные… Взяли и вшили в язык XML… Нужно, видимо, пойти дальше и встроить прям в язык Microsoft Office, почему нет? Строго внести в стандарт языка формат xls файлов… По началу, все это вызывает раздражение (еще и дурацкие названия стандартных классов – StringOps как вам?), кто-то плюнет и скажет, а ну ее эту скалу. А адепты презрительно пожмут плечами, мол, нехай сиволапый лезть в наши микросхемы, раз не понимаешь ни хрена, мы тут элита. Ну а если серьезно, обычные язык, без откровений, к сожалению, нет защиты от null pointer, только optional. Есть скользкие места и ловушки для новичков, я не уверен, что это хорошее решение для коллективного творчества.

Kotlin (1.2) – напомнил мне гнездо сороки: тут серебряная ложечка, тут фантик от конфетки… Девизом ребят, видимо, была песня «я тебя слепила из того что было». Взять из других языков проверенные временем решения и соединить, почему нет? Создатели языка называют это прагматичным подходом. Только иногда, как мне кажется, их подводил недостаток вкуса что ли (или чувства «прекрасного», на секундочку). Глядя на такую дичь как, return@forEach (прям e-mail какой-то) хочется выругаться интеллигентно и дать зарок не пользоваться этим. Но в целом язык мне все-таки понравился, лаконичный, все, что нужно есть. Жаль только присваивание в выражениях запретили и базовые типы не почистили как в Ceylon-е (вероятно, для меньших усилий при стыковке с Явой).

Fantom (1.0.69) – это такая «вещь в себе», фантомная, вроде бы уже существует с 2005 года, а вроде как и нет его. Взял в оборот исключительно из-за байки будто бы он рвет «как Тузик грелку» по производительности Яву на ее же JVM-е, ну и вообще, интересно было взглянуть на язык который компилируется в некие самопальные fcode, которые потом могут транслироваться как на JVM так и на CLR. Что сказать про сам язык? После того как я прочитал, что дженерики «пока еще не поддерживается», но это «пока» длится с каких-то там мохнатых годов и по-видимому так и останется, мой практический интерес к этому языку угас. Если быть точным поддержка дженериков там есть, но только для встроенных коллекций, а этого для инструмента разработки промышленного уровня, на мой взгляд, недостаточно. С одной стороны вроде бы есть nullable types – “Str? str”, но компилятор не препятствует обращению к str без проверки на null. Обидно…

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

• Интенсивный I/O
• Парсинг строк
• Вычисления с плавающей точкой с использованием математических функций: sin, cos, atan2, toRadians, sqrt
• Работа с коллекциями: ассоциативными массивами и просто массивами
• Лямбды (если есть)

Если в языке присутствовали свои библиотеки, я старался использовать их, если нет, то из Java.
Под руками оказались громадные текстовые файлы из предыдущего проекта с картографическими данными, поэтому тестовые приложения делают следующую работу. В командную строку предается путь к каталогу с файлами, участник должен получить список файлов отфильтровав их по расширению и обработать каждый. Обработка заключается в построчном чтении и парсинге строки и если это сегмент (т.е. отрезок на местности с координатами начала и конца), вычислить его длину и положить в map все сегменты которые находятся в определенном радиусе от заданной точки. Ключом в map-е служит идентификатор группы сегментов. Программа сама засекает время выполнения всей операции в миллисекундах и выдает количество найденных групп сегментов.

Самый лаконичный код получился на Kotlin, самый изящный и доступный для быстрого понимания на Ceylon.

Немного скучных цифр, объем кода в строках/килобайтах:

• Ceylon: 128/4.7
• C#: 177/6.2
• Fantom: 153/3.8
• Java: 203/6.1
• Kotlin: 117/3.9
• Qt/C++: 413/8.3
• Scala: 123/3.6

Объем получившихся бинарных (.class) файлов в килобайтах:

• Ceylon (*.class): 31.1
• C# (.exe): 8.7
• Fantom (.pod): 7.5
• Java (*.class): 9.9
• Kotlin (*.class): 20.9
• Qt/C++ (*.exe Release): 37.7
• Scala (*.class): 20.7

Ну а теперь самое вкусное, результаты забега… Кто прибежал первым, а кто оказался аутсайдером? Делайте ваши ставки, господа! Лично мои предсказания не оправдались, а результаты удивили. Тестирование проводилось на одной машине, но на двух платформах: Windows 7 x64 Professional и Linux Debian x64 Stretch Stable. И там и там была установлена Orcale Java 8.151. На Linux скомпилированный .exe файл C# запускался через mono из стандартного репозитория Debian. Запускалось все в одинаковых условиях на незагруженной системе. Я провел два раунда, в первом на входе был только один относительно небольшой файл 30Мб, в этом случае от запуска к запуску был некоторый разброс результатов, поэтому запускал по 10 раз каждого участника и брал лучший результат. Во втором раунде условия резко ужесточились, на входе было несколько файлов общим объемом около 900Мб. В этом случае разброс был значительно меньше, видимо из-за «прогретости» JVM-а. Но для чистоты эксперимента все-таки запускал несколько раз:

Windows 7 x64 Professional

Один файл (30Мб)

• Java: 609ms
• Scala: 901ms
• Kotlin: 961ms
• C# (MSVC2015, Relese): 1108ms
• Ceylon (JavaLibs): 1209ms
• Qt/C++ (MSVC2015 64-Bit, Release): 1329ms
• Fantom: 1352ms
• Ceylon: 1538ms

Несколько файлов (~900Мб)

• Java: 28103ms
• Scala: 30837ms
• C#: 32160ms
• Kotlin: 33580ms
• Fantom: 37063ms
• Qt/C++: 39058ms
• Ceylon: тест провален, участник не смог добежать до финиша и был принудительно снят с пробега по прошествии 5 минут работы над крупным файлом 738Мб (см. комментарий ниже).
• Ceylon (JavaLibs): 41392ms

Debian x64 Stretch Stable:

Один файл (30Мб)

• Java: 667ms
• Scala: 839ms
• Kotlin: 934ms
• Qt/C++: 1047ms
• Ceylon (JavaLibs): 1171ms
• Ceylon: 1323ms
• Fantom: 1332 ms
• C# (mono, Release): 2116ms

Несколько файлов (~900Мб)

• Java: 21380ms
• Scala: 25384ms
• Kotlin: 28860ms
• Qt/C++: 30823ms
• Ceylon (JavaLibs): 37485ms
• Fantom: 39872ms
• C# (mono, Release): 65463ms
• Ceylon: тест провален, участник испустил дух не добежав до финала с таким эпикризом: «Exception in thread «main» java.lang.OutOfMemoryError: GC overhead limit exceeded»

К сожалению, так понравившийся мне Ceylon (песня ты моя не спетая), еле-еле приплелся последним, отставание от Java существенное. Scala гнал ноздря в ноздрю с Java. Fantom естественно не произвел никаких революций. Java безоговорочный лидер по скорости, причем с существенным отрывом, а вот C# немного удивил, я думал, будет где-то наравне с Java. Совсем не ожидал, что нативный код окажется в хвосте! Можно, конечно, списать на неторопливость Qt, но все же… Не, ну можно, конечно, переписать на std либах или вообще на чистый C с ассемблерным вставками, но это уже будет совсем другой код. Была надежда, что молодой да ранний Kotlin покажет близкую с Java производительность, но отставание все же есть, значительно меньше, чем Ceylon, но больше чем Scala. Причем как-то непонятно где в Kotlin может проседать перформанс, ведь у него нет своего ввода/вывода, математической библиотеки и коллекций, все это используется из Java. Вот, например, Ceylon подвели самодельные коллекции, как выяснилось, когда я стал разбираться в причинах его провала на большом объеме входных данных. Они оказались слишком прожорливы к памяти, и съели 8Гб оперативки даже не заметив, поэтому тест и «подвис», например, при запуске на машине с 24Гб памяти, тест на Ceylon проходит нормально, но, конечно, абсолютные цифры сравнивать уже нельзя, это другое железо. Мне все же удалось заставить Ceylon работать, когда я заменил коллекции из его стандартной библиотеки на коллекции из Java (HashMap и ArrayList) и тест был пройден. Так же добавил результат для Ceylon когда используются Java библиотеки для I/O и коллекция, результаты немного лучше, но не спасают ситуацию.

Кстати, попутно можно отметить, что на одном и том же железе Debian обеспечивает лучшую производительность, исходные данные я подкладывал ему на ext4 раздел, а не брал их с NTFS.

Выводы

Как я уже говорил, из всех участников мне больше всего понравился Ceylon, на нем мне действительно захотелось покодить. Но, похоже, что он еще не созрел для промышленного использования, создателям нужно всерьез озаботиться профилированием и подчистить узкие места, тем не менее, я буду следить за его судьбой. Из оставшихся вариантов я бы выбирал между Scala и Kotlin, они, кстати, синтаксически весьма схожи, тот же а-ля паскальный стиль объявления функций и переменных. Scala лучше оптимизирован по скорости и более стабилен, но зато в Kotlin есть «умное приведение» и null-безопасность. Я, пожалуй, пока остановлюсь на Kotlin и буду изучать его детальнее, возможно, следующий внутренний проект мы напишем на нем.

Автор: Alexander Kornilov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js