Pull to refresh

Comments 43

Крутейшая статья.
Чуваку благодаря своему уму и знаниям глубин V8 удалось комбинацией улучшения алгоритма и глубинными оптимизациями догнать переписку с нуля Rust.
Вот ответ на эту статью от авторов переписки: http://fitzgeraldnick.com/2018/02/26/speed-without-wizardry.html
Они улучшили алгоритм в одном месте согласно советам mraleph и получили ещё х3.

Важнейший урок статьи: сначала проверь свой алгоритм.

Я бы процитировал эту часть:

Most of the improvements that mraleph implemented are desirable regardless of the programming language that is our medium. Excessive allocation rates make any garbage collector (or malloc and free implementation) a bottleneck. Monomorphization and inlining are crucial to eking out performance in both Rust and JavaScript. Algorithm transcend programming languages.

But a distinction between JavaScript and Rust+WebAssembly emerges when we consider the effort required to attain inlining and monomorphization, or to avoid allocations. Rust lets us explicitly state our desires to the compiler, we can rely on the optimizations occurring, and Rust’s natural idioms guide us towards fast code, so we don’t have to be performance wizards to get fast code. In JavaScript, on the other hand, we must communicate with oblique incantations to match each JavaScript engine’s JIT’s heuristics.

We chose to rewrite a portion of our library in Rust and WebAssembly not just for the speed ups, although those are certainly nice, but also for maintainability and to get rid of the kludges added to gain JIT optimizations. We wanted to return to clean code and clearly expressed intent. It has been a great success.

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

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


Единственная действительно спорная оптимзация состоит в использовании typed arrays руками — потому что она делает код действительно сложно читаемым. Но опять же — эту проблему можно решить просто добавив правильные фичи в JavaScript — а не приглашать людей переписывать их код на Rust.


Да и почему предполагется, что цена поддержки Rust и WASM нулевая? Это совершенно отдельный язык и экосистема! Автор Вы посмотрите на список вещей, которые нужно установить, чтобы обновить ту часть source-map, которая написана на Rust.

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

Очень рекомендую прочитать статью-ответ, ссылку на которую уже дали выше. Там подробно разбираются сделанные изменения при взгляде с обеих сторон и взвешиваются все «за» и «против».
Я на всякий случай напомню, что авторы библиотеки изначально так и делали.

Нет, ничего они так не делали изначально. Максимум они сделали:


  • хак с кэшированием (без которого на самом-то деле быстрее)
  • сделали свою собственную быструю сортировку

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


Очень рекомендую прочитать статью-ответ, ссылку на которую уже дали выше.

Я читал ответ, и я с ним не до конца согласен. (Если что, я это одна из двух сторон).

Ну и да, график тоже говорит сам за себя:

Сравнение производительности
image

Для полной объективности этому графику не хватает результатов JS-версии от mraleph. Они будут примерно на уровне зеленых точек (WASM без улучшения алгоритмов).


На таком фоне превосходство WASM будет не столь внушительно.

Еще раз: никто не говорит о превосходстве. Авторы хотели получить удобное и поддерживаемое решение. Они его получили. А то что оно оказалось еще и быстрым — приятный бонус.

Никто вас не заставляет это использовать только потому, что «это круто и раст всех порвет». Статья вообще не про это.

я отвечал на комментарий


Ну и да, график тоже говорит сам за себя:

А о чем он еще говорит, если не о превосходстве?

Вы знаете, забавный момент: в мире Rust как-то не принято выпячивать себя и кричать о производительности или выразительности.

Обычно стараются подчеркнуть не то, что Rust быстрее, а то, что он не медленнее. Не медленнее C/C++ при большей безопасности памяти и защите от состояния гонок; не медленнее JS, при большей прозрачности кода и т.д.

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

Я не пытаюсь Rust отодвинуть: у меня две причины, одна корыстная, одна не очень.


Во-первых, причина простая: я не понимаю зачем писать такие вещи на Rust, если можно на JS (который я не особо-то и люблю). Ну пусть бы написали что-то осмысленное и большое, но такую мелкую штуку смысла на мой взгляд не имеет никакого. Только из любви к языку, ну пусть тогда так и пишут — а не наводят тень на плетень.


Во-вторых, причина корыстная: в WASM компилировать по нормальному можно только языки типа C/C++/Rust, для всего остального надо за собой тащить собственный рантайм — GC, JIT, etc — скомпилированный в тот же WASM. Я считаю, что это неоправданные издержки. Поэтому получатся, что JS до сих пор единственный нормальный целевой язык для компиляции высокоуровневых языков (хотя он тоже не сахар, но хотя бы свой JIT/GC/exceptions, тащить не надо). А значит нужно поддерживать огонь под JS производительностью со всех возможных направлений.


(Что инетерсно тот товарищ, который source-map делает он даже написал собственный аллокатор памяти для WASM — потому что встроенный в Rust был слишком большой… ну у меня даже слов нет.)

(Что инетерсно тот товарищ, который source-map делает он даже написал собственный аллокатор памяти для WASM — потому что встроенный в Rust был слишком большой… ну у меня даже слов нет.)

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

Когда код компилируется в WASM управление памятью (по сути типизированным массивом) происходит на стороне js. Обычно сгенерированный файл js для управления памятью занимает очень много места, но иногда нам не нужна аллокация с дефрагментацией, и можно значительно упростить логику выделения памяти, тогда вместо 1500 строк кода сгенерированного аллокатора можно вложится в 100-200.
Понял, спасибо.
И этот большой аллокатор написан авторами wasm32-unknown-emscripten-тулчейна, понятно.
Все оптимизации достаточно примитивны — к тому же с течением времени необходимость в оптимизациях из группы №3 должна сойти на нет.

Для кого примитивны? Я бекенд-разработчик и JS знаю не так чтобы очень хорошо (хотя на бутстрапе и реакте что-то писал и правил), и для меня некоторые улучшения просто черная магия. Не говоря о том, каким образом получена эта информация — копание в ассемблерных листингах явно не моя сильная сторона, как думаю, и многих других разработчиков, особенно таких высокоуровневых языков вроде JS.


о опять же — эту проблему можно решить просто добавив правильные фичи в JavaScript — а не приглашать людей переписывать их код на Rust.

Добавление фич даже в раст, который выходит каждые 6 недель — это очень долгий процесс. Добавление фичи в JS я думаю займет не один год, причем до вашей фичи будет миллион других, более приоритетных. Это можно видеть и на примере других языков, например C#.


Да и почему предполагется, что цена поддержки Rust и WASM нулевая? Это совершенно отдельный язык и экосистема! Автор Вы посмотрите на список вещей, которые нужно установить, чтобы обновить ту часть source-map, которая написана на Rust.

Глянул, там говорится, что для компиляции rust нужно поставить компилятор rust. Ну офигеть. С тем же успехом вместо:


$ curl https://sh.rustup.rs -sSf | sh
$ rustup toolchain install nightly
$ rustup target add wasm32-unknown-unknown --toolchain nightly

там могло бы быть написано что-нибудь вроде:


$ curl https://nodejs.org/dist/v9.6.1/node-v9.6.1-linux-x64.tar.xz -sSf | tar xzf
$ pushd node-v9.6.1-linux-x64
$ xdg-open INSTALL
$ popd

что вообще не проблема, ибо делается на разработческой машине один раз за 5 минут.


В итоге, для меня очевидно, что раст более чем имеет смысл в такой задаче.


  • При написании кода на раст без использования какой-либо магии получился рост в 5 раз.
  • Для достижения похожего результата в случае с JS потребовался эксперт с огромным опытом, который потратил очень много времени и сил на оптимизацию. Не говоря про фразы в стиле "ну, я как раз вчера общался с разработчиками на похожую тему, и там в разговоре упомянули о новой функции/оптимизации Х, которую я сейчас и попробую заиспользовать". Не так много разработчиков сидят в кулуарах с разработчиками подобного софта, чтобы вообще знать о таких возможностях.
  • Ну и наконец, эксперт подобного уровня в экосистеме Rust смог получить прирост еще в 3 раза, или в 15 раз относительно оригинального JS кода.

В итоге, вывод статьи можно озаглавить, как "вы возможно можете добиться такого же прироста производительности в JS, если у вас есть в команде мэтр по JS и профайлингу уровнем не ниже какого-нибудь Саши Гольдштейна".


Мое мнение в итоге таково: раст, на таком уровне, чтобы переписать его так, как показано в оригинальной (неоптимизированной) статье, может изучить любой JS разработчик с опытом в 2-3 года. И соответственно написать такой код. Тот же разработчик с таким же опытом если дать ему то же время не сможет провести подобный анализ и оптимизацию, ему просто не хватит необходимых знаний.


Раст не надо совать везде. Но там, где он хорош — он действительно хорош. Это не инопланетный язык вроде J, код на котором пишется в режиме write only и для познания которого требуется вечность. Раст очень простой для освоения язык, потому что многие вещи в нем выводятся, а некоторые фичи нужны просто для производительности. Вам не нравятся лайфтаймы? В 99% случаев они выводятся, и вы даже можете не знать, что они используются. Не нравятся сложные трейты в стиле Into/AsRef/younameit Не пользуйтесь ими, аллоцируйте память на каждый вызов функции, это все равно будет выгоднее, чем можно было бы написать на JS, зато интерфейс останется простым и понятным. Список можно продолжить.


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

Не говоря о том, каким образом получена эта информация — копание в ассемблерных листингах явно не моя сильная сторона, как думаю, и многих других разработчиков, особенно таких высокоуровневых языков вроде JS.

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


и для меня некоторые улучшения просто черная магия.

Какие именно? Вот допустим пишете вы бэкэнд на C# или Java — для вас мономорфный/полиморфный код имеет точно такое же значение с точки зрения перформанса! Это никакая не черная магия.


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

Я потратил ~2 часа утром на диване перед работой, чтобы оптимизировать код. А затем я потратил гораздо больше времени, чтобы написать длинный пост с пошаговым объяснением.


Глянул, там говорится, что для компиляции rust нужно поставить компилятор rust

Там одним компилятором rust не обойтись, там еще всяких самописных тулов надо ставить.

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

Да ну хотя бы эта картинка
imagehttps://habrastorage.org/webt/bm/yt/sp/bmytspd4x3u5ymtabc26hcvbj9a.png


Я более чем уверен, что JS разработчик с пару годами опыта в такие дебри залезть не сможет. Точнее залезть, разобраться, и еще и правильные выводы сделать.


Когда я начал смотреть на цепочки вызовов для отдельных записей, я обнаружил, что немало из них проходит через KeyedLoadIC_Megamorphic в SourceMapConsumer_parseMappings.
Такая сортировка стеков вызова просигналила мне, что код исполняет множество сопоставлений по ключу в виде obj[key], где key — это динамически сформированная строка.

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


Какие именно? Вот допустим пишете вы бэкэнд на C# или Java — для вас мономорфный/полиморфный код имеет точно такое же значение с точки зрения перформанса! Это никакая не черная магия.

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


Я потратил ~2 часа утром на диване перед работой, чтобы оптимизировать код.

Я все еще считаю, что это ваша высокая квалификация. Моё почтение.


Там одним компилятором rust не обойтись, там еще всяких самописных тулов надо ставить.

Так же, как и в npm надо какие-нибудь модули доставлять: например, на винде npm при сборке падает с ошибкой если не установить самостоятельно питон. Всё еще не вижу проблем запустить один-единственный шелл скрипт на пяток строк, который за 5-10 минут скачает и установит все, что нужно.

Да ну хотя бы эта картинка

Эта картинка для объяснения "почему". Можно без объяснения "почему", просто сказать "делай как я! будет счастье" (и между прочим говорили начиная с 2010 года, я сам помнится говорил пару раз).


Тут надо повторится, что SpiderMonkey от такого не страдает и V8-товцы обещают подумать и починить.


Вот этот вывод например требует нехило так разбираться в предмете.

С этим я в принципе согласен. Нужно иметь хоть какой-то опыт чтения профилей, с учетом низкоуровневых деталей. Однако у меня есть такой пример: когда было дело и я общался с нодовцами по поводу производительности, то они обычно достаточно быстро просекали, как читать профили.


Я, кстати, об этом и в посте написал — что было бы очень хорошо если бы V8 умела все эти вещи объяснять понятным языком. Например, не KeyedLoadIC_Megamorphic, а что-нибудь более понятное человеку менее углубленному в детали реализации.

Эта картинка для объяснения «почему». Можно без объяснения «почему», просто сказать «делай как я! будет счастье» (и между прочим говорили начиная с 2010 года, я сам помнится говорил пару раз).

Кому можно? Я представляю просто ситуацию, где к разработчику пришли и сказали "работает медленно, ускорь". И у него два варианта, попробовать переписать на раст или попробовать оптимизировать так, как сделали вы. Он не может "сделать как вы" потому что он на своем месте, сам он не знает, как к задаче подступиться, а на stackoverflow такой вопрос не задашь.


Я, кстати, об этом и в посте написал — что было бы очень хорошо если бы V8 умела все эти вещи объяснять понятным языком. Например, не KeyedLoadIC_Megamorphic, а что-нибудь более понятное человеку менее углубленному в детали реализации.

Человеческое описание ошибок всегда плюс. Кстати, в расте имеется, что мне тоже очень понравилось, когда я начал язык изучать. Иногда можно просто скопипастить хинт из сообщения об ошибке так, чтобы все заработало. В примере выше это могло бы выглядеть как:


warning: comparator function has '3' arguments, but used with '2', which may cause performance issues.

function doQuickSort(ary, comparator, p, r) {
  // ...
      if (comparator(ary[j], pivot) <= 0) {
          ^^^^^^^^^^^^^^^^^^^^^^^^^
        // ...
      }

note: try use comparator(ary[j], pivot, 0)
Глянул, там говорится, что для компиляции rust нужно поставить компилятор rust.

Там нужно намного больше. Нужно склонировать отдельный репозиторий, собрать его, потом вручную скопировать WASM-бандл в основной репозиторий.


В результате мы имеем закоммиченный в git бинарник и никакой возможности автоматически пересобрать код одной командой.


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

Ну, в таком случае действительно неудобно. Ждем улучшения тулчейна.

Прошло полгода, и я сам окунулся в wasm. Правда у меня wasm это промежуточный этап компиляции в EVM, но все же.


Итого, чтобы скомпилировать WASM, мне нужно выполнить одну команду:


cargo build --release --target wasm32-unknown-unknown
wasm-build --target=wasm32-unknown-unknown ./target pwasm_tutorial_contract

(что и сделано в одном из моих проектов).


Никаких дополнительных репозиториев качать не надо.
Насчет того, чтобы сослаться на банлд из остальной части репозитория не вижу тоже ничего плохого и/или неправильного, а главное — сложного.

Это хорошо, что у Rust удобные инструменты. Но загвозка была в том, как подружить его систему сборки и npm-модули.

И ответ на этот вопрос есть вот здесь: github.com/rustwasm/wasm-bindgen/tree/master/examples/console_log

В том примере в одном проекте одновременно используется и Cargo.toml и package.json
> Автор Вы посмотрите на список вещей, которые нужно установить, чтобы обновить ту часть source-map, которая написана на Rust.

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

Да, и это работало. А вот попробуйте WASM в текстовом редакторе поправить. Еще та черная магия!

Я скорее про затраты на поддержание. Легко говорить о сложностях сборки, когда сам всеми силами их избегаешь и правишь итоговый результат :)
А если честно собирать и то, и другое, то вроде так на так и выходит, наверное.
В раннем рунете, еще до появления вездесущего JQuery, можно было встретить хорошее выражение:

Смысл революции не в том, чтоб «нам лучше было». Смысл революции — «чтобы вас, блюдей, не было»

Итак, цель протаскивания Webassembly и Rust в браузер — не в том, чтобы JS или кто-то еще улучшал свою производительность. Цель Rust в браузере — чтобы JS исчез, вместе со всеми ему присущими культурными явлениями, вплоть до вейпов и подворотов. Чтобы эта сущность была наконец засунута туда, откуда она вылезла, навсегда.

Ну это все фантазии, к сожалению, из разряда вечерних мечтаний за чаркой среди единоверов. "Ах вот бы было хорошо, если бы!"


Я бы может быть тоже хотел проснутся одним морозным утром и обнаружить, что JS скукожился. Но одним Rustом тут дело не решить, к сожалению. Нужен глобальный выбор — на чем хочешь, на том и пиши, а для этого нужен вменяемый compilation target. (… и все равно в конце-то внезапно обнаружится, что писать на все том же ЖС будут порядочно.)

эта сущность была наконец засунута туда, откуда она вылезла, навсегда
Плохо скрытый намек на превосходство Rust. Причем непонятна такая огрессия в сторону JS. Хороший специалист напишет хороший код на чем угодно (если это, конечно, возможно). Эта статья тому подтверждение. А индусы и самоутверждающиеся подростки с «вейпами» и «подворотами» никуда не денутся от глобального перехода на другой ЯП. Наверняка проредеют, но никуда не денутся. Зато у нас будет Rust и сломанная обратная совместимость. Что тогда? Будем ругать Rust и переходить обратно на JS?
охх чую, нахватаю я минусов от радикальных растеров и не только
Хороший специалист напишет хороший код на чем угодно (если это, конечно, возможно). Эта статья тому подтверждение.

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

С одной стороны можно порадоваться за v8, но с другой стороны — какой ценой, не лучше ли оптимизацию отдать компилятору, и сразу написать «не идеальный код» на rust, но быстро и который бы превосходил по скорости v8 из коробки. Сам поклонник nodejs но без фанатизма, там где надо быстрее — имеет место быть другому языку под конкретную задачу

Вот именно, под конкретную задачу лучше пользоваться одним языком. У node.js экосистемы уже был такой опыт: библиотека libsass на C++, которой предлагалось пользоваться из Node.js. В результате получился проект с сотнями issues, связянными с проблемами установки. И малое число внешних контрибьюторов, которые могли бы помочь исправить, потому что совсем незнакомый стек.


Поэтому SASS переезжает на JS (на самом деле Dart, но он будет компилироваться и поставляться в JS). Выигрыш в скорости не всегда оправдывает использование разношерстного мультиязычного стека.

Я может чего-то не понимаю, но разве инлайн функций не задача JIT компиляций? Или особенности javascript не позволяют JIT самостоятельно инлайнить функции?

UFO just landed and posted this here
JIT умеет инлайнить функции, но вызов функции должен быть мономорфным — те вы должны вызывать всегда одну и ту же функцию из данного места.
Мы только-только договорились с автором об официальном переводе этой статьи… С одной стороны, немного обидно, я уже начал работать над переводом. Даже не немного, а капец как.

С другой стороны — хорошо что хорошие статьи переводятся в таком быстром темпе.

Хорошо бы, чтобы у Хабра появилась фича, которая подсказывает, что статью «занял» другой переводчик.
надо было мне сказать, что ты будешь этот конкретный пост переводить, я бы justboris сказал, что кто-то уже переводит. а так ты меня спросил, можно ли статьи переводить и репостить — я сказал можно; но у меня это как-то в голове не сложилось 1+1, что ты эту конкретную статью будешь переводить. извиняюсь.
Проехали :) Я просто подпишусь на твой блог, и буду с максимальным 146% приоритетом переводить всё, что ты пишешь про компиляторы и рантаймы.
Очень интересная статья, спасибо за нее. Впервые узнал, что такое asm и вообще был удивлен, что в js можно так управлять памятью (но сам такого делать не буду). В разрабах я всего год и для меня то, что тут написано, просто невозможно. Хотя тема с кешами мне показалась очевидной, просто хотя бы потому, что там была огромная итерация и на каждую итерацию искался нужный кусок в огромном хеше. Часто пытаюсь открыть devtools, сделать perfomance и попытаться, что-то понять, но даже видя подсвеченные красным функции, не понятно, что с ними делать.
P.S. Статью читал 2-3 часа. Время потратил с пользой.
Sign up to leave a comment.

Articles