Pull to refresh
48
Karma
0.1
Rating
Alexey Andreev @konsoletyper

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

  • Followers 11
  • Following

JavaScript: заметка о WebAssembly

Это ещё поправят, думаю, компилятор ещё неплохо так оптимизируют

Увы, но нет. Пока с самой спекой wasm не сделают чего-то, никакой оптимизатор погоды не сделает. Есть множество причин, почему код wasm работает медленно, например:

  1. При любом доступе к куче делается bound check. Это в какой-то степени компенсируется оптимизатором, но очень часто у оптимизатора просто не хватает информации, чтобы понять, что проверку можно убрать

  2. В Wasm нельзя получить указатели на локальные переменные. Это значит, что если вы пишете код: "int a = 0; foo(&a);", то, кроме небольшого числа тривиальных кейсов, которые может распознать оптимизатор, копилятору придётся запрятать a в shadow stack, что и само по себе не быстро, а учитывая, что shadow stack находится в куче, доступ к которой проверяется, выходит совсем уж печально

  3. В Wasm нельзя походить по стеку. Это серьёзно ограничивает возможности по эффективной реализации GC и исключений. Что GC, что исключения, уже давно обещают, но воз и ныне там. И судя по черновикам, тот же GC (точнее, модель "кортежей") получится куцым и малопригодным для реальных нужд. С исключениями ситуация вроде получше (мне и черновик больше нравится, и шансов у него добраться до финальной спеки выше), так что возможно в каком-то обозримом будущем для кода на C++, активно использующем исключения, мы действительно увидим существенный прирост производительности.

  4. В Wasm нельзя делать трюки с memory protection, которые так же можно использовать, чтобы генерировать segfault при разыменовании нулевого указателя или переполнении стека.

Ну и т.д.

JavaScript: заметка о WebAssembly

Ваша методика бенчмаркинга не выдерживает никакой критики. Движкам с JIT необходимо прогреться, собрав статистику по выполнению кода и затем оптимизировав его. Соответственно, перед замерами тестируемый код надо погонять в цикле. Далее, сам замеряемый код так же необходимо прогнать несколько раз. То же самое желательно делать и с Wasm, т.к. никто не гарантирует, что конкретная среда не делает JIT. Далее, сам код - нарочито неоптимальный. По сути вы тут тестируете как быстро среда делает вызовы функций. Было бы интереснее что-то повнушительнее: рейтрейсинг, deflate, компиляция C, физический движок. Я понимаю, что для вводной статьи это слишком страшные вещи, так что хотя бы можно было потестировать на трёх версиях fib: вашей (рекурсивная, экспоненциальная сложность), рекурсивная с мемоизацией, итеративная.

«Почему Kotlin хуже, чем Java?»

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

Ну почему же. Вот, например. Правда, кросплатформенность и без Kotlin бывает, на обычной Java. Для iOS был MOE, а сейчас GraalVM. На Android и на сервере нативно работает. На web — TeaVM. Правда, на сервере не надо запускать весь код клиента, достаточно только части, отвечающей за OT.


Очень интрузивное и тяжелое изменение, которое заставляет код выполняться совсем не так, как написано, разрезая сверху до самого основания ваш код на suspendable и не-suspendable. Loom гораздо более элегантен и прозрачен.

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

«Почему Kotlin хуже, чем Java?»

Мутабельные данные — отличный способ отстрелить себе задницу, ящитаю.

Ахаха, вы это скажите сотрудникам Kotlin team, которые ловеринги пишут не путём "я пересоздам все IR элементы от рута до текущего элемента", а путём старого доброго изменения мутабельного свойства. Думаете, они там дураки сидят, которые не знают всех преимуществ иммутабельных данных и не могут в ФП? Боюсь, если бы они делали IR иммутабельным, kotlinc работал ещё раз в 100 дольше, чем он работает сейчас.


Ну а раз в любом случае это будет POJO с final-полями, то отчего бы не обмазать его префиксом data и не получить сахарок в виде copy-метода?

Так если это иммутабельный класс, то бога ради. Я просто вижу, как на вполне мутабельных расставляют. Впрочем, в Kotlin не всё так хорошо с иммутабельностью. Вот вам маленькая задачка от меня. Что выведет следующий код?


fun main() {
    val trollList = mutableListOf(1)
    val a = A(foo = trollList)
    val s = mutableSetOf(a)
    trollList += 2
    s -= a
    println(s)
}

data class A(val foo: List<Int>)

«Почему Kotlin хуже, чем Java?»

Как так, мы столько сил вложили в изучение джавы, стали синьорами, а нам тут говорят, что какой-то котлин лучше, чем наша дорогая джава?

Синьоры стали синьорами не потому, что все свои 10-15 лет в индустрии потратили на изучение синтаксиса и идиом конкретно взятого языка. Они изучали такие вещи как:


  1. Несколько других языков, хотя бы на базовом уровне. JS, чтобы что-то на фронте быстро подправить, не дожидаясь фронтэдеров (а то и вообще стали сами full stack). C++, чтобы написать performance critical код. C#, потому что в своё время успели на несколько лет переметнуться в .NET.
  2. Обширный набор фреймворков и библиотек для языка (стандартная библиотека Java, Spring, Hibernate, Jackson, log4j и т.д.)
  3. Базовые алгоритмы, структуры данных, паттерны, архитектура корпоративных приложений, объектно-ориентированный дизайн, принципы (SOLID, YAGNI, KISS, причём умение их использовать/не использовать на практике)
  4. Хотя бы общее знание предметной области (не одной).
  5. Инструменты для разработке на языке: отладка, профилирование, сборка, развёртывание.

Что приходится из этого проапдейтить при переходе с Java на Kotlin? Синтаксис. Ну может пару библиотек специально для Kotlin написанных посмотреть (ktor, exposed). Ну может научиться подключать модули к Spring, которые нужны для облегчения жизни Kotlin-истам. Ну просмотреть быстренько стандартную библиотеку Kotlin, увидеть, что она почти один-в-один является подмножеством стандартной библиотеки Java, плюс несколько расхождений (Sequence вместо Stream), плюс горстка удобств. Ну какие-то идиомы, которых нет в Java. Профайлеры те же. Отладчик тот же. Maven и Gradle те же. Поверьте, человеку с опытом в 10-15 лет в отрасли, всё указанное не так долго изучить. Вот перейти с Java на C++ или с Java на Python сложно. С Java на Haskell ещё сложнее.

«Почему Kotlin хуже, чем Java?»

благодаря этому они больше отвечают семантике "я строка в базе данных/элемент в очереди".

ИМХО, data классы и record-ы совсем не для этого были созданы. Как раз для строк в БД очень редко нужна специфическая семантика equals. Лично я считаю, что data class нужны ровно в двух ситуациях:


  1. Объявить иммутабельные штуки вроде Color, Vector2/Vector3, Rect, Point, Complex и т.д.
  2. Объявить кастомный составной ключ для Map/Set в случае, если Pair/Triple по какой-то причине не устраивают.

Собственно, поэтому мне на практике приходилось использовать data class-ы ОЧЕНЬ редко. Не понимаю, почему все бездумно лепят модификатор data на всякие DTO и entity (особенно на DTO)? Потому что есть ассоциация "данные == data"?

«Почему Kotlin хуже, чем Java?»

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

«Почему Kotlin хуже, чем Java?»

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

«Почему Kotlin хуже, чем Java?»

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

«Почему Kotlin хуже, чем Java?»

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

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


Есть inline, tailrec

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

«Почему Kotlin хуже, чем Java?»

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

«Почему Kotlin хуже, чем Java?»

Вставлю свои 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% безоблачно.

«Почему Kotlin хуже, чем Java?»

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

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

Что не так с WebAssembly?

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

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


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

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

Что не так с WebAssembly?

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

Что не так с WebAssembly?

Как автор компилятора, который может перегонять байт-код 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 в своё время.


Анбоксинг в современной Java

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

ВКонтакте снова выкладывает KPHP

не обладает всей "мощью" оптимизаторов которые были созданы для 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++).

ВКонтакте снова выкладывает KPHP

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

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 умеют ещё и редеплой делать очень быстро?

Information

Rating
3,456-th
Location
Санкт-Петербург, Санкт-Петербург и область, Россия
Date of birth
Registered
Activity