Pull to refresh
75
-1
Alexey Andreev @konsoletyper

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

Send message
В данном случае это java.

Поподробнее, пожалуйста. Почему на языки наклеивают ярлыки "это фронтэнд-язык, это — бэкэнд". Кто мешает взять CoffeScript или TS и писать на них код для node.js? Кто мешает взять GWT и писать на Java код для браузера? Да сейчас все кто не лень умеют компилироваться в JS, даже C++, даже Haskell, если не изменяет память. Так что же в языке может быть такого, что делает его более подходящим для бэкэнда и фронтэнда?

Самая большая глупость, которую можно сделать — это генерировать js из java кода

Почему генерировать JS из Java — это плохо?


Это кажется хорошей идеей, т.к. позволяет изучить только один язык и развиваться в нем.

Я для своего предыдущего проекта использовал GWT, и, поверьте, я отлично знаю JS и вполне мог бы взять Angular. Проблема в том, что мне неудобно постоянно переключать контекст. И вопрос не только в синтаксисе, вопрос в либах, фреймворках, сборщиках, линтерах, IDE и т.д. Ну и в ряде ситуаций и при прямых руках удаётся добиться переиспользования определённой доли кода.


Фронтенд уходит далеко вперед, а технологии, которые занимаются извращением — не могут поспевать за этим всем.

Какие извращения? И в чём проблема с поспеванием? Из GWT можно отлично вызывать любой JS-код, самый новомодный и хипстерский.


Мой позиция в том, что бекенд (в виде java) не должен заниматься рендером html,

Есть разные задачи, в каких-то случаях лучше доверить рендеринг HTML бэкэнду, а на фронтэнде не держать почти никакой логики.


и уж тем более не должен заниматься кодогенерацией js

А GWT, к примеру, этим не занимается. GWT работает во время компиляции, а в рантайме вы имеете статический JS.


А вот генерировать js — это и вовсе плохая идея, по-моему, и ни одной адекватной причины для этого не вижу.

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


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

Это зависит от архитектуры. Если проект грамотно разбит на слои, то никаких проблем прокинуть два разных presentation (один — HTML, другой — REST) нет. Конечно, хотелось бы писать весь код один раз и запускать везде, но практика показывает что это невозможно. Хотя опять же, есть ряд задач, когда можно добиться почти 100% переиспользования кода (те же игры, например).


И да, к GWT это никак не относится, потому что GWT не занимается рендером страниц.

Количество боли от конкретно GWT не является показателем того, что Java — это плохой язык для разработки фротнэнда. Это является лишь показателем того, что гугл этот самый GWT поматросил и бросил, не влил достаточного количества ресурсов, чтобы сделать из него конфетку. В последних версиях GWT преобразился (появилась инкрементальная компиляция, нормальный интероп и отладка), и количество боли от его использования стало заметно меньше. Если бы для него кто-то запилил что-то похожее на Angular — было бы вообще отлично. Но теперь-то кому какое до этого есть дело?

А можно подробнее про то, почему GWT — это плохая технология? И что такое "бэкэнд-язык"? Какие особенности синтаксиса и семантики языка делают его пригодным для написания кода на фронтэнде, а какие — на бэкэнде?

Продвинутые пацаны используют пулы. Пишете веб-сервер, например, и выделяете буфер, скажем, в мегабайт на обработку одного запроса. После этого аллокация — это тоже инкремент одного значения.

По-научному это называется "region-based memory management". Есть такая штука — вывод регионов, когда компилятор вычисляет, где какие регионы создать, удалить и какие объекты в какой регион поместить. Существуют языки с явной поддержкой регионов (например, Cyclone), где разработчик может давать подсказки компилятору по поводу регионов. Существуют компиляторы для "традиционных" языков, которые делают вывод регионов (например, некоторые реализации JVM). Есть класс задач, на которых регионы дают хороший прирост производительности, а есть задачи, на которых от них нет совершенно никакого преимущества.


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

Хороший инкрементальный GC делает это достаточно рано. Для задач, где в young generation выживает сравнительно немного объектов, мы получаем преимущество по сравнению с ручным управлением памятью. Чаще всего в типичных приложениях это условие выполняется.


Обычно локальность (мне) нужна тогда, когда есть N мегабайт или гигабайт чисел, и надо их перемолоть. Во всех остальных случаях это не столь важно.

Локальность тут неприменима, ибо эти гигабайты чисел не влезут в кэш. А вот если есть небольшой (пара мегабайт) граф, и мы по нему несколько раз обходим, то хорошо, если второй, третий и т.п. обходы попадут в кэш. А это как раз характерно для типовых приложений. Например, получили пачку данных из БД в виде графа объектов и ходим по ним, считаем бизнес-логику. Ну или в компиляторах, взяли IR функции и анализируем, применяем интринзики, оптимизируем (несколько раз), наконец обходим, чтобы сделать понижение и кодогенерацию.


И что за проблемы с дефрагментацией кэша в 2016 году и 64-битным адресным пространством?

В моём оригинальном комменте есть пара ошибок. Там где я написал "дефрагментация кэша" следует читать "дефрагментация данных". Если мы всё время лезем в объекты, расположенные в памяти далеко друг от друга, у нас старые данные вытесняются из кэша CPU.

То есть вы сравнили потоконебезопасный GC с malloc'ом, который, не исключено, бОльшую часть времени тратит именно на блокировку. Не очень справедливо.

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


И стал бы он у вас, скорее всего, ещё медленнее malloc'а.

Неправда, потому что malloc занимается только выделением памяти, а не освобождением. В данном случае надо сравнивать скорость работы free и GC. Так вот про это я опять же всё написал: free освобождает объекты по одному, GC может освобождать сразу тысячи и даже миллионы объектов.


В общем, это полностью отдано на откуп JVM, и вполне может быть реализовано как на C++.

Разумеется, в Java всё отдано на откуп конкретной реализации JVM, как и в C++ всё отдано на откуп конкретной реализации компилятора. Мы говорим о практически хороших реализациях.


Но, например, в Orcale'овской реализации сделано так: указатель на инстанцию объекта (включая, видимо, и this) — это, на самом деле, пара указателей: собственно указатель на данные, и указатель на описание класса (в котором есть указатели на методы)

Странная у вас информация. В HotSpot указатель — это такой же указатель, как в C++ (ну или в случае 64-битной архитектуры и включённого compressed oops — (int) (указатель >> 3)). Другое дело, что первые 8 или 16 байт объекта отводится под заголовок, в котором, в числе прочего, есть ссылка на класс, который содержит так же и virtual table. Это нужно, чтобы правильно делать invokevirtual. Для реализации invokeinterface используются различные интересные методики, но им опять же хватает одной ссылки на класс из заголовка.


В C++ ровно та же история: первые 4/8 байт у обычного объекта — это vtable_ptr. А вот у объекта с multiple inheritance должно быть n таких vtable_ptr (соответственно, 4/8 * n байт оверхеда).


Жалко, что такое нельзя сделать для C++, т.к. иначе, видимо, поломается ABI старых C-шных либ.

Дело не в том, что поломается ABI, а в том, что по-другому просто нельзя (или можно, но будет ещё больший оверхед по памяти или по производительности).


(если JVM, конечно, поддерживает такой инлайнинг).

Вот откуда идут все утверждения по поводу тормознутости Java. Люди просто не знают, каковы возможности C2 в HotSpot, и не желают знать.

И какого же? А разве GC не должен искать блок подходящего размера? Откуда он возьмёт сам указатель на выделяемый блок памяти?

Не должен. Получить значение очередного блока памяти — это просто инкремент. Даже проверять, что память исчерпана не надо — вместо этого можно просто сделать trap на segfault.


Что касается дальнейшего описания — вы точно сделали настоящий, потокобезопасный GC?

Нет, GC непотокобезопасный, неинкрементальный, и вообще очень тупой. Однако, это не относится к теме. Я на различных бенчмарках гонял реализацию на malloc, и на gc_alloc, и тормоза были (как показал мне gprof) именно на вызовах malloc. Прирост был не за счёт освобождения памяти, а за счёт аллоцирования (кстати, в своём GC я сделал оптимизацию, которая умеет освобождать неиспользуемые объекты сразу большими группами, если они расположены друг за другом, привет free).


Если бы я хотел сделать GC, дружественный с многопоточным окружением, я бы


  1. Сделал многопоточный аллокатор. Врочем, там один только инкремент — т.е. он уже многопоточный.
  2. Реализовал бы safe points, научил бы GC ждать, когда все потоки дойдут до safe points. Это очень даже небольшой оверхед, особенно если грамотно расставлять эти самые safe points.
  3. Т.к. у меня GC и так простейший и тупейший, ну ok, я бы сделал, чтобы все потоки ждали, пока GC отработает, сам бы GC работает в один поток.

Может быть, имеются в виду shared pointers? Потому что в smart никаких счётчиков нет, ибо на один объект может быть только один указатель.

Ну я не разбираюсь в конкретных терминологиях конкретных stdlib-ов (в stl используется одно название, в QT — совсем другое, но я уже забыл, где что). Я знаю, что есть такая концепция умных указателей, я так же знаю, что есть куча разных модификаций и оптимизаций под конкретные ситуации.


А как НЕперемещающий GC может сделать данные более локальными?

Никак, но в реализации JVM, о которой говорят в 95% случаев (т.е. от Oracle) GC перемещающий. Да в любой другой уважающей себя то же самое. Кроме, разве что, какой-нибудь кровавой эмбедщины.


При множественном наследовании нет никакого оверхеда. Данные объектов идут один за другим в памяти (возможно, с выравниванием). Вы не путаете с виртуальным наследованием?

Я не знаю, что такое виртуальное наследование. Я знаю, что для поддержки виртуальных вызовов используется механизм virtual tables. Так вот проблема в том, что у экземпляра класса, который наследуется от N других классов, будет N ссылок на virtual table. В Java есть свои сложности в реализации invokeinterface, но они решаются, причём без загаживания объекта списком vtableptr.


Можно как минимум в Visual Studio 2012 (и позже). Но, естественно, только если транслируются исходники модулей — из dll ничего заинлайнить нельзя. Про gcc не в курсе.

Можно в определённых средах, которые требуют особой упаковки библиотек. Не только VS, ещё и clang так может, если правильно собирать модули. Проблема в том, что в системе 100500+ имеющихся библиотек, которые уже упакованы в DLL или so.

У C++ есть свои проблемы с производительностью, делающие его медленнее, чем Java, на целом классе задач. Вот что сходу вспомнил:

1. Если вы аллоцируете много объектов (а сложное приложение аллоцирует много объектов), то в C++ запускается malloc, работающий сравнительно медленно. В GC аллокатор — это просто инкремент одного значения. Я писал свой игрушечный компилятор JVM в LLVM, и на первое время не написал свой аллокатор, а использовал malloc. Потом написал простецкий GC, и получил колоссальный прирост (от 2 до 5 раз) на различных задачах. Если бы я был отделом разработки Oracle, то я бы написал очень шустрый GC и всё стало бы работать ещё быстрее.
2. Если у вас сложный граф объектов, то в C++ придётся использовать smart pointers. Это дополнительный расход памяти и дополнительные расходы на инкремент/декремент счётчиков. Да, в GC есть write barriers, но они не пишут в память, т.е. они быстрее (и локальнее по памяти).
3. GC (особенно перемещающий) делает ваши данные в памяти более локальными. В C++ есть серьёзные проблемы с дефрагментацией кэша, с которой отчаянно пытается бороться malloc. Привет, CPU cache!
4. При использовании множественного наследования в C++ получается два негативных эффекта: увеличивается размер заголовка каждого экземпляра такого класса, появляется оверхед на операцию == в некоторых случаях. В Java такой проблемы нет, т.к. множественное наследование возможно только на интерфейсах, и поэтому оно реализуется гораздо эффективнее.
5. В C++ нет возможности заинлайнить функцию из одного модуля в функцию из другого (если только не используется clang и модули не распространяются в виде биткода). Я так понимаю, основная масса моего /usr/lib — это всё-таки не биткод LLVM.

Если вы пишете криптографию или видеокодек, то данные проблемы вас не касаются (правда, и C++ вас не касается, тут уже ближе к C с ассемблером). В сложных больших приложениях, вроде всякого энтерпрайза или IDE, всё это обычно встаёт в полный рост, и использовать Java предпочтительнее, именно с точки зрения производительности.
В качестве примера — моя сегодняшняя работа (работаю уже 2 недели) была получена благодаря двум волшебным словам — «микросервисы» и «докер» (с наглядной демонстрацией своих скилзов в этой области) — задача перевести существующее монолитное приложение на архитектуру микросервисов. Деплоймент на AWS (собственно он и сейчас там)… 700 евро плюс (в смысле это повышение моей зарплаты по сравнению с предыдущем местом работы)…

Вот приходили ко мне люди, знающие «волшебные слова», а когда я их просил написать разворот списка — сливались. Изучить такие вещи на базовом уровне (чтобы показать скилы на интервью) опытный разработчик способен за пару недель, ну месяц в крайнем случае. Тем более, что в том же docker нет ничего принципиально нового для человека, который имел дело с OpenVZ пару лет в продакшене. А вот что действительно нужно — фундаментальные знания, светлая голова и прямые руки — никак не зависит от новых парадигм и shiny new things.
Хочется верить, что я действительно преувеличил и такие утилиты существуют, но среди бесплатных такого еще не встречал

Про .NET не знаю, но для Java есть мой проект, который очень хорошо вырезает неиспользуемый код.

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

Проблема в том, что зачастую тратится очень много времени на коммуникацию с этими веб-программистами. Кроме того, в последнее время веб усложняется и требования к квалификации веб-программистов растут.

Во-вторых, транслятор сам по себе может содержать баги, обнаружить и устранить которые — отдельное приключение.

Транслятор (в моём случае) написан на Java, которая содержит баги, которая в свою очередь линкуется с glibc, который содержит баги и который запускается на Linux, который содержит баги и который запускается на железе, которое, представьте себе, тоже содержит баги.
Но автоматический транслятор так не работает. Он видит, что вы использовали метод StartsWith — значит в вашем приложении используется класс System.String, реализацию которого нужно перенести целиком. Открываем содержимое класса с помощью ILSpy и видим добрые две сотни методов!

Это неправда! Хороший вырезатель зависимостей работает совсем не так как вы думаете! Агрессивный dataflow-анализ способен творить чудеса.

Процесс определения зависимостей крайне трудоемок. Нет способа быстро определить, на какие классы ссылается определенный класс в ходе своей работы.

Это правда. К сожалению, задача определения зависимостей плохо масштабируется, параллелится и плохо дружит с инкрементальной компиляцией. Тем не менее, процесс определения зависимостей нужен только для сборки production, там не сильно принципиально, что компилятор проработает минут 10 на CI-сервере. А во время разработки определять зависимости не обязательно.

Невольно возникает вопрос — почему транслятор такой тупой? Мы же в 2016 году живем, у нас тут каждый второй — программист, работающий в коворкинге над стартапом по краудфандингу лэндингов

Потому что чтобы написать прототип для какого-нибудь стартапа, вовсе не нужны фундаментальные знания, которыми часто почему-то пренебрегают. Достаточно большая доля кандидатов, которых мне приходилось собеседовать (думаю, где-то половина), не справилась с простой задачкой разворота списка. Это уж не говоря про какой-нибудь поиск в графе в глубину или быструю сортировку.

а написать идиоматичный код на JS нельзя

А это и не требуется. Для программ на 10 строк вовсе не обязательно использовать трансляторы с «тяжёлых» языков вроде Java или C#. А для более серьёзных по объёму программ разница становится не слишком существенной.

Да можно, конечно, но это потребует титанических усилий и никак не окупится.

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

Изучить родные веб-технологии и переписать приложение на них все равно окажется проще.

Изучить родные веб-технологии действительно несложно, но тут есть другой фактор. Если у меня уже есть тонна кода для серверной части, которая при этом обвешана различными тулами для сборки проекта, статической проверки кода и т.д. то хотелось бы их по-максимуму переиспользовать для клиентского кода, а не выстраивать параллельно всю инфраструктуру фронтэнда с нуля. Да и частично переиспользовать сам серверный код на клиенте тоже очень приятно.
Есть различные техники, чтобы очень аккуратненько вырезать всё то, что ненужно. Мой проект использует такие техники, так что на выходе генерит достаточно маленький JS со всеми необходимым либами.

Конечно, всё это хорошо работает только в отсутствие reflection. Есть устоявшийся стереотип, что без reflection некоторые вещи написать невозможно. На самом деле, я разработал альтернативный механизм, на котором я успешно реализовал такие традиционно требующие reflection вещи, как сериализация/десериализация объектов в JSON и REST-клиент. Конечно, переиспользовать существующий код с reflection не получается, но всё равно, это гораздо лучше, чем TypeScript, в котором вообще нельзя переиспользовать код C#.

Правда, получается подмножество стандартной библиотеки, но оно достаточно большое, чтобы можно было писать вполне себе серьёзные приложения, и при этом получать достаточно маленькие JS-файлы.
Классический reduce получает на вход три параметра: исходное значение, бинарную функцию и список. Если бинарная функция является ассоциативной, то reduce можно распараллелить, иначе — нельзя. Проблема в том, чтобы автоматически доказать ассоциативность переданной функции. Вторая проблема в том, что есть неассоциативные функции, к которым хочется применить reduce (и это никак не оптимизируешь).

Далее, указанная оптимизация касается только специально написанной функции fold, про которую компилятор заранее знает и заранее может её оптимизировать таким путём. А как быть с общим случаем, когда компилятор ничего не знает про природу fold? Как компилятор, посмотрев на реализацию fold, должен понять, что к ней можно применить такую оптимизацию?
Ещё как важен! Взять ту же функцию reduce (fold, aggregate). Если переданная ей ФВП не обладает «хорошими» свойствами типа ассоциативности или коммутативности, порядок вычисления возможен только один. А поди ещё докажи эти свойства в компиляторе: чаще всего это в общем случае неразрешимая задача. А для частных случаев оптимизации прекрасно работают и в императивных языках.

Кстати, вот reduce — это частный случай. А как в общем случае заставить компилятор понять, что если мы в функцию передаём что-то ассоциативное, то её можно по-другому посчитать?
Вот только когда состояние есть (объективно, присутствует в требованиях), то поступают вот как: берут функцию, которая принимает предыдущее состояние и трансформирует его в следующее состояние. Haskell умеет это дело аккуратненько прятать, заворачивая в монады и засахаривая do-синтаксисом. Проблема в том, что получившееся дерево комбинатором можно редуцировать только в определённом порядке (только «хребет» графа). Распараллелить можно только если относительно каких-то подграфов можно доказать, что их можно редуцировать параллельно это никак не затронет семантики редукции. Так вот отказывается, что и в теории и на практике это сделать ничуть не проще, чем для привычного императивного CFG доказать, что его куски можно исполнять параллельно.

Синхронизация всё равно возникает, ведь есть функция, порождающая новое состояние из старого. А что если есть целый список таких состояний, функция список превращает в список. «Состояния» из этого списка, возможно, будут иметь какие-то «общие данные», доступ к которым придётся синхронизировать.
Можно подробнее про ручной костыль, задумки и трудности?
Совершенно не понятно зачем на VM тащить мусор вроде inline. Это сугубо работа компилятора и нечего лезть к нему со своими указами. Если нужно выжимать максимум, для этого есть куда более подходящие языки программирования.

тут
Ну в философском смысле можно развести долгую дискуссию насчёт state/identity, о том, что у иммутабельных объектов их нет, и поэтому сравнение можно производить только по совокупности полей, хотя иммутабельные объекты — это никакие не объекты, а хак в ООП для эмуляции value. Проще говоря, есть примитивы вроде int/long — есть ли у них состояние? А что если мы хотим ввести свои «примитивы» вроде Vector, ComplexNumber, Address? Кстати, один такой «примитив» есть в JDK — String. Есть же объекты, которые меняют состояние, поэтому сравнивать их по состоянию некорректно, т.к. они могут иметь одинаковое состояние в какой-то момент времени, но это не означает, что это один и тот же объект. Пример — близнецы: у них очень многие параметры похожи, их даже (теоретически) могут назвать одинаково, но это всё-таки два разных человека, т.к. у них есть identity.

С практической точки зрения переопределение equals/hashCode, которые нестабильны (т.е. изменяются со временем жизни объекта) может привести к поломке HashSet/HashMap. Да и контракт hashCode явно требует его стабильности. Зависимость hashCode от состояния объекта (т.е. от поля, объявленного как var) как раз делает hashCode нестабильным.
То ли я что-то в этой жизни не понимаю, то ли data class в Kotlin совершенно неверно позиционируют. А именно, повсюду повторяют примерно такую мантру:

Представляют интерес Data Classes. Данные классы используются для хранения данных и больше ничего не делают.


На самом деле, для этого не надо никаких data class. Например, вот такой класс:

class A(var u: Int, var v: String, var w: Boolean)


На самом деле, есть такое понятие как value object (лучше почитать DDD Эванса), и вот, мне кажется, что data class — это отличный способ реализовать данный паттерн. На каждом углу кричат, что value object должны быть immutable, и я с этим мнением вполне согласен (не хочу приводить целую пачку аргументов здесь, кому надо — гуглите по ключевым словам value object immutable). Так вот, отсюда вытекает, что

1. Если уж не запрещать var в декларации data class, то хотя бы надо выдавать предупреждение, включённое в настройках intentions по-умолчанию
2. В документации к языку и официальных блог-постах тщательно объяснять этот момент.
3. Отписываться в комментах к статьям вроде этой, которые, о ужас, предлагают объявлять entity как data class.

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

К сожалению (а может, и к счастью), тут нельзя, как в некоторых языках, явно задать приоритет инфиксной функции.

Information

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

Specialization

Specialist
Senior
From 6,000 €
Java
Compilers
Kotlin
Gradle