Предисловие

Я, как истинный почитатель javascript и typescript, просто не мог пройти мимо шикарной статьи на медиум, в которой можно найти много интересных особенностей структур памяти, оптимизации производительности процессора и как с ними связан javascript? В данной статье идет речь о пари сеньора с его подопечными и как этот вызов привел автора к глубокому разбору соотношений javascript'a и Rust'a на поле сражения под названием производительность и память.

Давайте поговорим о производительности

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

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

Прекрасным примером является случай, когда зеленый разработчик оспаривает ваши рекомендации (в реальности, будучи старшим разработчиком, вы всегда делаете неправильный выбор в глазах других) и делает ставку на то, что его подход — лучший подход. Я знаю далеко не все, но я достаточно долго в этом деле, чтобы увидеть лопуха. Как я мог сопротивляться? Я приму это пари. И спустя годы я напишу об этом пост.

Пари

Я, честно говоря, не помню особенностей(прошло несколько лет), но помню, что рекомендовал использовать Node.js в первую очередь исходя из набора знаний существующей команды разработчиков, доступных библиотек и прочих технических нужд. Младшие разработчики хотели показать «безумные» навыки модных бакалавров компьютерных наук. Может быть, они знали, что я не очень хорошо разбираюсь в компьютерных науках, и предположили, что я просто не догадываюсь, как на самом деле работают компьютеры (честно говоря, спустя ~ 20 лет я пришел к выводу, что это просто магия).

Утверждение было чем-то вроде стандартного «C++ быстрее, чем Javascript», которому противостоял мой (стереотипно сеньорский) ответ: смотря по обстоятельствам. Или, возможно, более конкретно, «оптимизированный C++ будет работать лучше, чем оптимизированный Javascript», поскольку запуск Javascript сопряжен с неизбежными накладными нагрузками(ну, вы, вероятно, могли бы скомпилировать его в статическую программу и получить аналогичную производительность, если бы очень-очень постарались). Излишне говорить, что мне нравится хорошие вызовы.

И в результате...

«Сюрпризом» было то, что решение на javascript оказалось немного быстрее, чем программа на C++, и (что более важно с архитектурной точки зрения) имело то преимущество, что оно было полностью совместимо с техническими особенностями проекта. Пусть бакалавры молчат и чешут затылки. Честно говоря, я не был на 100% уверен, что javascript выиграет, но, основываясь на вероятной зависимости этого конкретного варианта использования от объектов памяти с динамическим размером и неопытности разработчика, я сделал обоснованное предположение.

Подожди-ка, но как?

Если вы не можете догадаться "почему?", не волнуйтесь. По моему опыту, большинство разработчиков тоже не знают почему. Результат противоречит общему правилу, согласно которому скомпилированные языки работают быстрее, чем интерпретируемые, а статические программы быстрее, чем динамические. Но это всего лишь эмпирическое правило.

«Оптимизированный» — ключевое слово в моем ответе выше, поскольку наивная программа на C++ может быстро сойти с рельсов. С другой стороны, Node.js (использующий V8 и libuv на основе C++/C) добился больших успехов в оптимизации глупого JS для быстрой работы, а это означает, что есть случаи, когда наивный JS может превзойти наивный C++. Но это явно сложнее.

Ах да, память...

Большинство разработчиков должно быть знакомо с идеями стеков и куч, но многие не углубляются в поверхностные характеристики, такие как линейность стека и куча с указателями (или что-то в этом роде). Они также, вероятно, упустили из виду, что это всего лишь концепции (а есть и другие подходы) с несколькими реализациями. Аппаратное обеспечение низкого уровня обычно не знает, что такое, черт возьми, «куча», поскольку программное обеспечение определяет, как управляется память*, и сделанный выбор может иметь огромное влияние на характеристики производительности конечной программы.

* Есть целая кроличья нора, в которую можно (и, возможно, нужно) спуститься. Ядра могут быть сложными, а современное оборудование далеко не глупым и часто может включать в себя ряд оптимизаций специального назначения, которые могут использовать высокоуровневые схемы памяти в своих оптимизациях. Это может означать, что программное обеспечение может (или вынуждено) делегировать функции управления памятью, предоставляемые оборудованием. И это даже не касается виртуализации…

Наши дни: вход в Rust

Сегодня Rust — один из моих любимых языков. У него много замечательных современных функций, он быстрый и имеет отличную модель памяти, что позволяет создавать в целом безопасный код. Конечно, у него есть недостатки, время компиляции по-прежнему является проблемой, и тут и там присутствует странная семантика, но в целом я настоятельно рекомендую его.

Один из проектов, над которым я сейчас работаю, — это хост FaaS (функция как услуга), написанный на Rust, который выполняет функции WASM (WebAssembly). Он предназначен для очень быстрого безопасного выполнения изолированных функций, минимизируя накладные расходы на использование FaaS. И это довольно быстро, способно получить 90 тысяч чистых запросов в секунду на ядро. Более того, он может сделать это с общим объемом эталонной памяти ~ 20 МБ.

Какое это имеет отношение к Node.js и C++? Ну, я использую Node.js в качестве эталона для «разумной» производительности (Go используется как цель «мечты», его трудно сравнивать с языком, разработанным для веб-сервисов, добавляя накладные расходы производительности на FaaS), и ранние версии программы не были многообещающими (хотя они использовали менее 10% памяти Node.js).

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

Я люблю "кучи", я возьму две (или три)!

По сути, куча — это просто некоторая память, которой управляет аллокатор . Программа запрашивает N единиц памяти, и аллокатор найдет их в своем доступном пуле памяти (или запросит у хоста выделение дополнительной памяти), запомнит, что единицы используются, и затем вернет указатель местоположения этой памяти. Когда программа завершает работу с этой памятью, она сообщает аллокатору, а аллокатор затем обновляет свое отображение, чтобы знать, что эти единицы теперь доступны. Просто, верно?

Проблемы начинают возникать при выделении множества единиц памяти разного размера с разным временем жизни, вы в конечном итоге получите большую фрагментацию, которая увеличивает стоимость выделения новой памяти. Именно здесь вы начинаете замечать снижение производительности, поскольку это, по сути, собственная программа, просто чтобы выяснить, где хранить вещи. Очевидно, что у этой проблемы нет единого решения, существует множество различных алгоритмов распределения от buddy system до slabs и блоков. У каждого подхода есть компромиссы, то есть вы можете выбрать, какой из них лучше всего подходит для вашего варианта использования (или просто выбрать вариант по умолчанию, как это делает большинство людей).

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

Для моего проекта FaaS мы создали динамический аллокатор, который выбирает алгоритм распределения на основе использования, и этот выбор сохраняется между запусками. Для «малоиспользуемых» функций (по-видимому, большинства функций на данный момент) используется наивный аллокатор стека, который просто поддерживает один указатель на следующий свободный слот. При вызове Dealloc, если модуль является последним в стеке, он просто откатывает указатель, в противном случае это noop. Когда функция завершена, указатель устанавливается на 0 (например, выход Node.js перед сборкой мусора). Если функция достигает определенного количества неудачных операций освобождения памяти и определенного порога использования, для остальных вызовов используется другой алгоритм распределения. Результатом является очень быстрое выделение памяти в большинстве случаев.

Существует также еще одна «куча», используемая во время выполнения, а именно хост — разделяемая память функции. Хост использует ту же стратегию динамического распределения и позволяет производить запись непосредственно в память функции, минуя этап копирования в ранних версиях. Это означает, что ввод-вывод напрямую копируется из ядра в гостевую функцию, минуя среду выполнения хоста и значительно повышая пропускную способность.

Node.js vs Rust

После оптимизации среда выполнения Rust FaaS стала на > 70 % быстрее при использовании на > 90 % меньше памяти, чем наша эталонная реализация Node.js. Но соль в том, что «после оптимизации» первоначальная реализация была медленнее. И это потребовало наложения некоторых ограничений на работу функций WASM, хотя они прозрачно применяются во время компиляции с редкими несовместимостями.

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

У нас запланированы дополнительные оптимизации, но в основном они связаны с изменениями на уровне хоста, которые имеют серьезные последствия для безопасности. Они также не имеют прямого отношения к производительности управления памятью, но дают много пищи для лагеря «Rust быстрее, чем Node».

Заключение

Не уверен. Я предполагаю пару моментов:

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

  • Я до сих пор использую (и рекомендую) и Node.js, и Rust для разных целей, так что тут тоже нет никакой победы. JavaScript замечательно переносим и отлично работает для множества облачных сценариев, но Rust — отличный выбор, когда действительно важна производительность.

  • И всякий раз, когда я говорю JavaScript, я на самом деле имею в виду TypeScript. Я ведь не дикарь.

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

Благодарю за внимание!

Комментарии (27)


  1. MentalBlood
    14.07.2022 21:34
    +55

    А где же код или ссылки на него?..


  1. cepera_ang
    14.07.2022 21:56
    +66

    Статья про производительность без единого замера или примера кода, вот это поворот.


    Чувствую себя обманутым, потому что ждал раскрытия того, что написано в заголовке, а получил какие-то общие слова про аллокаторы памяти.


    1. cepera_ang
      14.07.2022 22:00
      +22

      и как этот вызов привел автора к глубокому разбору соотношений javascript'a и Rust'a на поле сражение под названием производительность и память.

      И это ещё глубокий разбор. Прости господи, что же для этого архитектора — поверхностное ознакомление?


  1. Hrodvitnir
    14.07.2022 21:58
    +34

    Я знаю далеко не все, но я достаточно долго в том деле, чтобы увидеть лопуха. Как я мог сопротивляться? Я приму это пари.

    В том деле – может все таки в этом? Увидеть лопуха? А как долго надо быть в том деле, чтобы увидеть лопуха?
    О каком пари речь? Может все таки о вызове?

    Очень двоякое ощущение – вроде все понятно, но так глаза режет.
    Ладно, просто охлажу траханье.


    1. sergegers
      15.07.2022 16:00
      +2

      углепластик


  1. Hrodvitnir
    14.07.2022 22:06
    +1

    Тоже как-то баловался с С (с переводом в WASM, разумеется) и нодой и выяснил, что как бы я не изголялся с оптимизацией алгоритмом поиска подстроки в строке – версия на ноде выходила раза в 2 быстрее. Потому что много времени тратилось на копирование строки в память WASM.
    И только на безобразно длинных строках скорости начинали сравниваться.

    Эх, и WASM не панацея:(


    1. Alexufo
      15.07.2022 17:28
      +1

      А какой смысл поиск строки в подстроке в васме, если js и wasm работают в одной виртуальной машине? Поиск же это по оперативной памяти, а не тяжёлые вычисления. Мультипоточность в васме использовали?


  1. OpenA
    14.07.2022 22:08
    +5

    Не прочитал ничего нового. Небольшие программки на жаваскриптах работают иногда шустрей не то что плюсового, даже сишного кода, засчет паерекомпиляции налету с профилем исполнения этого кода. Но на больших данных (ну как больших, для джаваскрипта все что > 1k уже большое) джаваскрипт обсирается. Куча статей на эту тему было.


  1. smartello
    14.07.2022 22:08
    +25

    Резюме: Rust быстрее, но можно суметь написать на нём так, что будет медленнее.

    всякий раз, когда я говорю JavaScript, я на самом деле имею в виду TypeScript

    Это видимо написано для рекрутеров, ведь остальным понятно что никакого typescript в рантайме нет.


    1. Jian
      15.07.2022 06:26
      -1

      Резюме: Rust быстрее, но можно суметь написать на нём так, что будет медленнее.

      Так и на java-script можно тоже написать, так что будет всё тормозить. :)


  1. Source
    14.07.2022 23:15
    +9

    Да уж. Одно предложение "Хороший код на языке X быстрее плохого кода на языке Y" размотали на целую статью. Вот это поворот.


  1. thatsme
    14.07.2022 23:22
    +23

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

    Я сломался сразу на этой фразе. GPT-3 перелогинься.


    1. AirLight
      15.07.2022 01:46
      -1

      Я не споткнулся. В былые времена, когда памяти было мало, какие-то десятки мегабайт - это соотношение было понятнее. Условно, можно хранить данные зазиповано - и еще неизвестно повысится ли скорость обработки или понизится. Если процессора много, а памяти мало - зипуем. Если памяти много, а процессора мало - храним в плоском виде. Что такое много и мало тут величина очень зависит от контекста задачи.


    1. Lazytech
      15.07.2022 07:14

      Цельнотянутый машинный перевод с небольшой рихтовкой, однако. В оригинале всё хорошо.


  1. 4reddy
    15.07.2022 01:43
    +2

    Это шутка такая что ли? Ну очень сомнительная статья...
    Обо всём и ни о чём.

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


    1. Hrodvitnir
      15.07.2022 05:14
      +3

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

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


      1. vassabi
        15.07.2022 10:14

        ой, вам бы только запрещать.

        Рейтинг и карма - это мягкие руководства к действию. Если бы у вас были четкие критерии, что такое "хорошая публикация" - то еще может быть. А пока у нас есть только кое-какие эвристики что такое "плохая публикация" - то лучше использовать карму и рейтинг и смотреть длинные тенденции.


        1. Hrodvitnir
          15.07.2022 10:58
          +4

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

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

          Так что да, я бы такое запретил


  1. Mitai
    15.07.2022 07:19
    -4

    очередная статья пытается убедить людей что жабаскрипт хороший


  1. HemulGM
    15.07.2022 08:43

    Нативный автор перевода сомнительной статьи)


  1. JordanCpp
    15.07.2022 09:17

    Автор узнал о концепции аллокаторов и применил какой нить linear allocator со сложность O(1)? Сколько же удивительных открытий еще предстоит сделать автору статьи.:)


  1. paveltyurikov
    15.07.2022 09:50
    +2

    Правильно ли я понял, что в итоге JS оказался на 70% менее производительным и на столько же более прожорливым по памяти?


    1. Druj
      15.07.2022 14:57
      +1

      Кто-то ожидал другой вариант?


  1. garbagecollected
    16.07.2022 07:56

    А вот вам еще один пример сравнения JS и Rust.


    1. Alexufo
      16.07.2022 14:19

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


  1. ormuzd
    16.07.2022 21:23

    Гугл переводчик переводил? Читать невозможно.


  1. Eremite_b
    16.07.2022 21:23

    Что мы должны понять из дедушкиных рассуждений? javascript ещё можно использовать, и даже в виде TypeScript. Аминь