Pull to refresh
75
-1
Alexey Andreev @konsoletyper

Пользователь

Send message

Это скорее не то, чтобы прекрасная идея, это back to roots. Потому что в своё время странная идея "а давайте писать тип переменной перед её именем" под влиянием языка, который (по другим причинам, а не из-за этой идеи) стал популярным, пошла в массы. Кажется, до народа дошло, и новые языки как раз возвращаются к нотации, появившейся чуть ли не раньше компьютеров (например Kotlin, TypeScript, Rust, Swift).

Для того, чтобы утвержать такое, надо вначале определиться, что мы называем ФП, потому что какого-то единого чёткого критерия нет. Если мы договоримся, что среди наших критериев есть ленивость, то да, с данным тезисом я соглашусь. Однако можно долго спорить, включать ли критерий ленивости в определение ФП. Всё-таки Ф — это про функции, а не про ленивость (иначе бы у нас был ЛП). А так получится, что только Haskell можно назвать полноценным ФП, а какие-нибудь OCaml или SML идут лесом.

Да условному синьёру не сложно выучить Kotlin. Может, он его уже отлично знает. Просто оказывается так, что из-за ряда факторов Kotlin вместо того, чтобы увеличивать эффективность сеньёра, снижает её. Лично для себя я просто выделил кейсы, когда Kotlin помогает, а когда мешает, и использую или не использую его в конкретной ситуации, исходя из этого понимания. Речь о том, что Kotlin на данный момент не может покрыть 100% (или даже 90%) ситуаций, когда он был бы однозначно лУчшим выбором, чем Java.

Вообще в IDE все настраивается и с kapt.

Можно ссылку? Просто я с ходу не нашёл. А для IDEA вообще ничего не надо настраивать — она сама при импорте проекта из maven/gradle обнаруживает annotation processors и подключает их.


Есть inline, tailrec

Никогда не понимал смысла в оптимизации хвостовой рекурсии. Есть же нормальные циклы. Это, конечно, очень увлекательное упражнение для ума — переписать всякие map/filter/fold на чисто функциональном языке, где циклов нет и нет ленивых вычислений, так, чтобы они были tailrec. Ещё было в студенческие годы не менее увлекательно написать библиотеку функций на SK комбинаторах. Однако на практике я вообще не встречал ни единой ситуации, когда написать функцию с хвостовой рекурсией было бы более просто и наглядно, чем написать банальный цикл.

Первое упоминание об IR появилось году так в 2017-м, когда его запилили для нужд Kotlin Native. С тех пор команда плавно переписывает все бэкэнды на IR, параллельно этот самый IR допиливая. К тому времени, как Kotlin 1.5 будет с нами и как все бэкэнды окончательно переделают в IR, и когда этот IR стабилизируется и появится стабильное же API для работы с ним из плагинов, и всё это ещё аккуратно поддержат в IDEA, вот тогда и поговорим (сколько ещё ждать, год, два три?). Тогда я признаю, что одной проблемой в Kotlin меньше. А пока мы имеем дело с тем, с чем имеем, и пункты 1 и 2 заставляют меня не переходить на Kotlin в тех проектах, где эти пункты являются критичными.

Вставлю свои 5 копеек, почему Kotlin может быть хуже Java


  1. Нет адекватной замены annotation processors. В Java процессоры подтягиваются в IDEA и когда я жму build, они автоматом запускаются и (пере)генерируют нужный код. Если меняются классы, на которые смотрит процессор, то мне не надо запускать какую-то специальную тулзу — я просто жму run на нужной run configuration и магия сама работает. Конечно, из-за того, что Sun (а теперь Oracle) в своё время не продумали возможности работы с IDE, которая инкрементально запускает javac, это в редких случаях ломается, но в целом всё в разы лучше, чем в Kotlin, где единственная альтернатива — вручную запускать kapt.
  2. Скорость работы компилятора. Сколько бы не говорили про то, что она приближается к работе javac, на деле там отставание не в 3 и не в 5 раз, а раз в 10-20. Это на реальном коде, с которым мне приходится работать, а не на каком-то синтетическом примере, для которого приводят бенчмарки для сравнения скорости компиляторов. Конечно, Kotlin умеет компилировать инкрементально, НО! Инкрементальная компиляция часто ломается во всяких интересных случаях (например, когда Kotlin модуль зависит от Java модуля, в котором сделали совсем небольшое невинное изменение).
  3. Производительность сгенерированного кода. В подавляющем большинстве случаев это вообще не является проблемой, но есть специфические сценарии, где производительность важна и даже на Java пишут в C-стиле (потому что оптимизатор JVM слишком туп), и порой то, как генерирует код kotlinc, так же вносит свои 10-15% в снижение производительности. Особенно это важно для сред, которые плохо умеют в оптимизацию и имеют слабое железо (Android). Например, все JVM-декларации функциональных типов объявляют параметры Object и возвращаемое значение Object. Поэтому даже если функциональный тип в конкретном use site параметризован non-null kotlin.Int, мы всё равно наступим на boxing.
  4. Nullability в некоторых случаях мешает. Например, мы можем какое-то время иметь частично инициализированную структуру, но потом мы в какой-то момент её достраиваем и точно знаем, что что-то — гарантированно не null. Java вообще никаких гарантий не даёт, так что все такие вещи делаются на уровне комментариев, документации в коде и т.д. Kotlin даёт гарантии, при этом!!! считается как бы дурным стилем и его принято стараться избегать. В этом ключе описанный мой случай приводит либо к обилию использования этого самого !!, либо к злоупотреблению lateinit, который может давать странные эффекты.
  5. Отсутствие package private. Тут всё неодназначно, потому что в Java отсутствует internal, а порой его так же не хватает (но есть всякие OSGi, которые в каком-то смысле решают эту проблему).
  6. Правило final by default. Конечно, про это уже 1000 раз говорили, для spring написали модуль, для jackson тоже, так же для Kotlin написали allopen. Однако, всё же всплывают то тут то там какие-то проблемы. Я понимаю благородные намерения авторов языка, однако, тут сложно сказать что лучше — сделать, как правильнее или как совместимее.

Короче, мне, как разработчику, не слишком интересно, запишу я код метода в 3, 5 или 10 строк. Мне важно, как удобно со всем этим будет работать в связке с другими инструментами из экосистемы Java. И у Kotlin с этим, пусть всё и хорошо, но не на 100% безоблачно.

Например, изрядное количество подробностей внутренней работы kotlinc скрыто внутри сгенерированных файлов классов, представляющих из себя аннотации @Metadata с бинарными данными (байтовыми массивами, разрешёнными в аннотациях) внутри. Насколько мне известно, эти данные не описаны ни в каких публичных спецификациях.

Кстати, их не то, чтобы совсем сложно распарсить. Это на самом деле protobuf, proto-файлы находятся в репозитории Kotlin. Их можно аккуратненько скопировать себе и сгенерировать код, который парсит данные. Там остаются кое-какие нюансы, которые можно узнать, почитав код компилятора Kotlin, но в целом задачу "прочитать метаданные Kotlin" я в своё время осилил.

В тех примерах, что я смотрел, компиляторы C++ стараются избегать передачи через стек, используя locals

Так это и возможно сделать только если параметр передаётся в виде value. А если в виде ссылки, то единственный сценарий, когда, КМК, это можно оптимизировать — если компилятор решил заинлайнить функцию. Во всех иных ситуациях если вы явно попросите у C++ передать указатель или ссылку на переменную в стеке, то он и передаст указатель или ссылку соответственно. Это не особо возможно оптимизировать.


Если все-таки стек нужен, то считается, что указатель на вершину стека хранится по смещению 4 в памяти

Что по сути и есть тот самый медленный shadow stack взамен быстрого нативного.

Ну и хотел бы так же добавить, что в итоге идея "скомпилировать байт-код в JS", хотя и кажется людям, далёким от всей этой кухни, каким-то ужасным хаком, на деле оказывается гораздо более жизнеспособной, чем компиляция в Wasm.

Как автор компилятора, который может перегонять байт-код JVM в WASM могу вставить свои 5 копеек.


  1. Нет доступа к стеку. Делали так из благих побуждений — безопасность и простота реализации виртуальной машины. На деле, в C++ вполне обычная ситуация, когда надо вызвать функцию, передав ей указатель на локальную переменную. Честно говоря, что генерируют компиляторы C++ в Wasm, я не смотрел. Да и в Java так делать нельзя. Зато в Java есть GC, у которого roots находятся в стеке. Как я это обошёл? Завёл shadow stack прямо в хипе. Да, я придумал хитрый алгоритм, который вычисляет для каждого call site минимальный объём обновлений shadow stack. Но всё равно это медленно.


  2. Нельзя поиграться с memory protection. Есть очень много сценариев использования оного. Самый простой — проверка указателей на null. В реализациях C++ или Java здорового человека принято первую страницу делать недоступной и поэтому при попытке записать или прочитать по адресу 0, CPU бросает исключение, которое можно поймать в виде, например, сигнала unix. В Wasm такие трюки не работают и приходится просто перед каждым dereference (например, чтением поля объекта) вставлять проверку. Кстати, т.к. доступа к стеку нет, то приходится в shadow stack маркировать любой такой доступ, чтобы при выбросе исключения правильно воссоздать stack frame.


  3. Казалось бы, нам обещают GC и exception handling в будущих версиях. Но вот я, честно говоря, слабо верю в то, что какая-то прибитая гвоздями спецификация GC сможет учесть разнообразие поведения разных VM в различных экзотических ситуациях, например, с разнообразными weak reference.


  4. Ещё одна претензия к черновику GC: приходится дублировать заголовок. Для нужд GC в Wasm необходимо объявлять типы данных и при аллокации объекта (tuple) в управляемом хипе указывается этот тип. Понятное дело, что физически при этом какое-то количество байт будет отведено на указатель на тип данных. Далее, чтобы реализовать виртуальные вызовы, мне так же надо в объекте хранить указатель на virtual table. Получается, у меня двойной заголовок объекта, потребляющий в два раза больше памяти. Это в то время, как у нормальных людей заголовок объекта сразу и vtable описывает и layout объекта для GC.


  5. Отсутствует вообще какая-либо стандартная библиотека или хотя бы набор инструкций для некоторых важных операций, вроде копирования участка памяти, обнуления участка памяти, работы с плавающими числами (например, какой-нибудь isNaN) и т.д.


  6. Какой-то очень мутный застрявший процесс. Вот вышла несколько лет назад спека 1.0 и дальше комитет только заседает и заседает, генерирует какие-то кривые черновики, а воз и ныне там. Похоже на ситуацию с застрявшей спекой XHTML в своё время.


Я как человек с компиляторным бэкграундом и хорошо знающий, как работает JIT с его куриными мозгами, не стал бы в критическом коде писать новомодный синтаксис, а всё бы развернул руками. Ещё есть Android (а так же различные способы запустить Java-приложения на iOS) и там уж точно всё работает совсем не так. Так что если нужно писать кроссплатформенный код (например, библиотеку, которую можно использовать на бэкэнде и на Android), да ещё и критический по производительности, я бы точно воспользовался старым добрым синтаксисом. Такие мелочи не особо влияют на читаемость кода, а IDE позволяет быстро его писать. Вот что действительно плохо влияет на читаемость кода — это более глобальные вещи, вроде правильных абстракций, соблюдения принципов вроде SOLID и т.д., а не какие-то отдельно взятые циклы.

не обладает всей "мощью" оптимизаторов которые были созданы для C/C++

Боюсь, у меня тут недостаточно экспертизы, т.к. я не копался во внутренностях оптимизаторов C++, но исходя из моей практики написания AOT-компилятора для Java, который специально делался с глобальными оптимизациями и исходя из информации, прочитанной мной из статей на эту тему, время, необходимое более-менее умным алгоритмам чтобы оптимизировать хорошо, устремляется в бесконечность. Поэтому за счёт чего может выиграть AOT-компилятор — у него время самой компиляции не ограничено, поэтому можно применить просто чуть более мощные алгоритмы, дающие чуть лучшую производительность в некоторых ситуациях (например, graph-coloring register allocator вместо linear scan register allocator).


JIT (и даже AOT) в Java и других подобных языках работает в пределах одной функции

JIT и AOT-компиляторы в Java очень хорошо инлайнят. Проблема только в том, что чтобы заинлайнить вызов, он должен быть мономорфным (ну или биморфным, если конкретный компилятор умеет). А алгоритмы, которые могут хорошо доказать мономорфность вызова, ну очень требовательны к ресурсам и нигде не применяются, насколько мне известно. Поэтому применяются более практичные и топорные алгоритмы, которые, увы, дают весьма консервативную оценку. Это я говорю как человек, написавший сравнительно неплохой девиртуализатор для Java. Так вот, у JIT с этим проблем нет вообще, т.к. они смотрят, какие классы БЫЛИ на callsite-е в реальности во время прогонов интерпретатора (с деоптимизацией, если предположение оказалось неверным и повторной оптимизацией под новые реалии).


Можете привести хотя бы один такой случай как пример? Только при условии что код не использует ничего кроме стандартных возможностей языка, т.е. CPU-bound.

Нет, не могу. У меня были примеры в моей практике, но код, увы, закрыт. А лезть в интернет и искать бенчмарки я не хочу. Так вот, в моей практике было, что числодробильный код, написанный на Java и на C++ работал с одинаковой производительностью, если его скомпилировать gcc. И C++ проигрывал Java при компиляции clang и msvc. Код на C++ писали люди, которые хорошо владеют C++ (а не просто джависты, которые дорвались до C++).

Если бы это было так, то компиляторы ушли бы в небытиё.

AOT-компиляторы не ушли в небытиё, не потому, что они генерируют более производительный код, а потому что в их случае нет рантаймового оверхеда, связанного с необходимостью где-то держать исходный код и/или промежуточное представление, считать профиль и т.д. А так же AOT позволяет сильно снизить время прогрева приложения. В случает, например, Java, производительность кода, порождённого AOT-компиляторами ниже, чем JIT.


Пока же код (кроме самого простого) написанный на C/C++ или даже Java/.net/Go превосходит по скорости выполнения любой JIT для большинства языков которые его поддерживают.

Это некорректное утверждение. Не бывает просто "более быстрого" кода, бывает код, который лучше ведёт себя в тех или иных задачах, и бывают случаи, когда Java уделывает C++ (а бывает и обратное). Вообще, C++ и Java просто разные языки, созданные для решения разных задач и под разное мышление программиста. Корректно сравнивать AOT и JIT компилятор для одного языка. И C++ уделывает Java только потому, что первый более низкоуровневый и позволяет вручную контролировать вещи, которые Java не позволяет (и поэтому, при грамотном подходе из C++ выжимается большая производительность)

Ну а как же скорость сборки? Дев сервер на CI собирается? Сколько времени на это уходит? А как насчёт того, что IDE пересобирает только изменившиеся файлы (например, 1 из 100К файлов в проекте)? А как насчёт того, что некоторые известные нашлёпки на IDE умеют ещё и редеплой делать очень быстро?

Может это и так в каких-то других языках программирования, но это явно не про джаву.

Ну скажем так, дьявол в деталях. В Java действительно всё с этим очень хорошо, но порой можно попасть в какие-то те самые 0.01% случаев, где начнётся ад.


Странно что вы osx и линукс в корзину кладёте. Это вообще-то разные системы. И большинства таки либо Windows либо OsX

Я ссылаюсь на статью автора, где указано, что проект заводится под Linux, с трудом — под Mac и вообще без надежд — в Windows. Что там у большинства в Яндексе, я могу только гадать, и вообще, не зная их реалий не могу сказать, почему так. Я лишь называю одну из возможных причин.

Ну так написание, отладка, тестирование и поддержка этих if...else — это человекочасы, которых в конкретном случае может и не быть. Видимо, люди сопоставили тот факт, что у большинства и так linux или macos, а поддержка windows выйдет дороже, чем перевод на linux/macos тех, кто предпочитает windows.

Очевидно что эта библиотека кросплатформенная и называется JDK. Ибо функции inotify в Java выполняет WatchService.

Вообще-то, с WatchService всё не так хорошо. Например, иногда в Windows она норовит на файлы повесить share mode при котором другие процессы ничего с этими файлами сделать не могут. Потом в том, как приходят события в WatchService, в Windows есть свои интересные нюансы (не помню точно, какие, сталкивался с этими особенностями пару лет назад).


Откройте какой-нибудь условную обёртку над нативыми библиотеками, например Xerial и удивитесь на скольких платформах она работает

А если нет такой условной обёртки? Или условная обёртка не подходит под нужды? Ну например, разместить text input поверх GL окна, при этом имея полный доступ к конфигурации GL-контекста этого окна? К сожалению, ни SDL, ни GTK этого делать не позволяют, так что то, что в Windows решается штатным Windows API, в Linux делается за счёт продирания через особенности интеграции GTK с wayland или X.org.

Абсолютно согласен, но это не стопроцентная зависимость. Т.е. бывают в принципе адекватные вежливые люди, которые верят во всякую дичь (по крайней мере, с моей точки зрения).

псевдонаучность и веру в городские легенды куда?

Мне самому это не нравится, но, ИМХО, это вовсе не повод минусовать автора. Вот если он агрессивно продвигает свои идеи, никого не слушает и т.д — это повод (но вообще, это повод вне зависимости от "научности" статьи). А если статья мне не понравилась, но автор не грубит и не упирается рогом, то я просто минусую статью, но не карму.

Иными словами нельзя просто так взять и выбросить создание массива, т. к согласно спецификации исполнение обязано бросить NegativeArraySizeException и ничего мы с этим поделать не можем:

Ну или код по созданию массива можно заменить на код, который проверяет длину и кидает исключение, если она отрицательная. Это небольшой оверхед, но это всё равно намного лучше, чем создавать ненужный массив в куче.

Information

Rating
Does not participate
Location
München, Bayern, Германия
Date of birth
Registered
Activity

Specialization

Specialist
Senior
From 6,000 €
Java
Compilers
Kotlin
Gradle