Написание высокопроизводительного Javascript кода без деоптимизации - PullRequest
10 голосов
/ 09 марта 2020

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

  1. Мы всегда хотим, чтобы наши массивы были упакованы SMI (маленькие целые числа) или упакованы как Doubles, в зависимости от того, выполняем ли мы вычисления целых чисел или с плавающей запятой.
  2. Мы всегда хотим передавать функции одного и того же типа функциям, чтобы они не были помечены как «megamorphi c» и деоптимизированы. Например, мы всегда хотим вызывать vec.add(x, y) с упакованными SMI-массивами x и y или обоими упакованными двойными массивами.
  3. Мы хотим, чтобы функции были максимально встроены.

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

  1. Вы можете превратить упакованный массив SMI в упакованный массив Double с помощью, казалось бы, безобидной операции, например, эквивалентной myArray.map(x => -x). На самом деле это «лучший» плохой случай, поскольку упакованные массивы Double по-прежнему очень быстрые.
  2. Вы можете превратить упакованный массив в обобщенный c в штучной упаковке, например, сопоставив массив с функцией, которая (неожиданно) вернул null или undefined. Этого плохого случая довольно легко избежать.
  3. Вы можете деоптимизировать целую функцию, например vec.add(), передав слишком много типов вещей и превратив ее в мегаморфи c. Это может произойти, если вы хотите заняться «обобщенным c программированием», где vec.add() используется как в тех случаях, когда вы не проявляете осторожность в отношении типов (так что он видит много типов, которые входят), так и в случаях, когда вы хочу добиться максимальной производительности (например, он должен получать только двойные числа в штучной упаковке).

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

  • Есть ли где-нибудь руководство по программированию, оставаясь в мире упакованного SMI? массивы (например)?
  • Возможно ли выполнить обобщенное c высокопроизводительное программирование в Javascript без использования чего-то вроде макросистемы для встраивания таких вещей, как vec.add() в места вызова?
  • Как можно объединить высокопроизводительный код в библиотеки в свете таких вещей, как мегаморфизация c сайтов вызовов и деоптимизация? Например, если я счастливо использую пакет линейной алгебры A на высокой скорости, а затем я импортирую пакет B, который зависит от A, но B вызывает его с другими типами и неожиданно деоптимизирует его (без мой код меняется) мой код работает медленнее.
  • Есть ли какие-нибудь хорошие простые в использовании инструменты измерения для проверки того, что движок Javascript делает внутри с типами?

1 Ответ

8 голосов
/ 11 марта 2020

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

Есть ли где-то набор руководств по программированию, оставаясь в мире упакованных массивов SMI (например) ?

Краткий ответ: это прямо здесь: const guidelines = ["keep your integers small enough"].

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

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

Возвращаясь к примеру под рукой: предполагается, что внутреннее использование Smis - это деталь реализации, о которой пользовательский код не должен знать. Это сделает некоторые случаи более эффективными, и в других случаях это не должно повредить. Не все движки используют Smis (например, AFAIK Firefox / Spidermonkey исторически нет; я слышал, что в некоторых случаях они используют Smis в настоящее время; но я не знаю никаких деталей и не могу говорить с какими-либо полномочиями по причина). В V8 размер Smis - это внутренняя деталь, которая на самом деле менялась с течением времени и в разных версиях. На 32-битных платформах, которые использовались в большинстве случаев, Smis всегда были 31-битными целыми числами со знаком; на 64-битных платформах они были 32-битными целыми числами со знаком, что в последнее время казалось наиболее распространенным случаем, пока в Chrome 80 мы не отправили «сжатие указателей» для 64-битных архитектур, что требовало уменьшения размера Smi до 31 биты известны из 32-битных платформ. Если бы вы основали реализацию на предположении, что Smis, как правило, 32-битные, вы получите неприятные ситуации, такие как this .

К счастью, как вы заметили, двойные массивы все еще очень быстрый. Для числового кода, вероятно, имеет смысл предполагать / предназначаться для двойных массивов. Учитывая преобладание двойных чисел в JavaScript, разумно предположить, что все движки имеют хорошую поддержку для двойных и двойных массивов.

Возможно выполнение обобщенного c высокопроизводительного программирования в Javascript без использования чего-то вроде макросистемы для встраивания таких вещей, как ve c .add () в места вызова?

"generi c", как правило, расходятся с "высокой производительностью". Это не связано с JavaScript или с конкретными реализациями c движка.

Код "Generi c" означает, что решения должны приниматься во время выполнения. Каждый раз, когда вы выполняете функцию, код должен запускаться, чтобы определить, скажем, «является ли x целым числом? Если да, то взять этот путь кода. Является ли x строкой? Затем перепрыгнуть сюда. Является ли это объектом? у него есть .valueOf? Нет? Тогда, может быть, .toString()? Может быть, в цепочке прототипов? «Высокопроизводительный» оптимизированный код по существу основан на идее отбросить все эти динамические проверки c; это возможно только тогда, когда у движка / компилятора есть какой-то способ заранее выводить типы: если он может доказать (или предположить с достаточно высокой вероятностью), что x всегда будет целым числом, то ему нужно только сгенерировать код для этот случай (защищенный проверкой типа, если были задействованы недоказанные предположения).

Встраивание ортогонально всему этому. Функция «generi c» все еще может быть встроенной. В некоторых случаях компилятор может распространять информацию о типе во встроенную функцию, чтобы уменьшить там полиморфизм.

(Для сравнения: C ++, являющийся статически скомпилированным языком, имеет шаблоны для решения связанной проблемы. Короче говоря, они позволяют программисту явным образом указывать компилятору создавать специализированные копии функций (или целых классов), параметризованных для данных типов. Это хорошее решение для некоторых случаев, но не без собственного набора недостатков, например, большого времени компиляции и больших двоичных файлов. JavaScript, конечно, не имеет такой вещи как шаблоны. Вы можете использовать eval для создания система в чем-то похожа, но тогда вы столкнетесь с похожими недостатками: вам придется делать эквивалент работы компилятора C ++ во время выполнения, и вам придется беспокоиться об огромном количестве кода, который вы генерируете.)

Как сделать модульный высокопроизводительный код в библиотеках в свете таких вещей, как мегаморфизация c сайтов вызова и деоптимизация? Например, если я с удовольствием использую пакет линейной алгебры A на высокой скорости, а затем я импортирую пакет B, который зависит от A, но B вызывает его другими типами и деоптимизирует его, внезапно (без изменения кода) мой код работает медленнее .

Да, это общая проблема с JavaScript. V8 использовался для реализации определенных встроенных функций (например, Array.sort) в JavaScript внутри, и эта проблема (которую мы называем «загрязнением обратной связи типа») была одной из основных причин, почему мы полностью отошли от этой техники.

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

Кроме того, внутри движка гораздо больше ситуаций, чем "один тип = = быстро "и" более одного типа == медленно ". Если данная операция видела и Смиса, и двойников, это совершенно нормально. Загрузка элементов из двух видов массивов тоже подойдет. Мы используем термин «megamorphi c» для ситуации, когда нагрузка видела так много разных типов, что она отказывается от отслеживания их по отдельности, а вместо этого использует более универсальный механизм c, который лучше масштабируется для большого количества типов - функция, содержащая такие нагрузки, все еще может быть оптимизирована. «Деоптимизация» - это очень конкретный акт c необходимости выбрасывать оптимизированный код для функции, потому что виден новый тип, который не был замечен ранее, и что оптимизированный код, следовательно, не приспособлен для обработки. Но даже это хорошо: просто go вернитесь к неоптимизированному коду, чтобы собрать больше отзывов о типах и оптимизировать позже. Если это случится пару раз, тогда не о чем беспокоиться; это становится проблемой только в патологически плохих случаях.

Итак, краткое изложение всего этого: не беспокойтесь об этом . Просто напишите разумный код, пусть двигатель справится с этим. Под «разумным» я подразумеваю: то, что имеет смысл для вашего варианта использования, является читабельным, обслуживаемым, использует эффективные алгоритмы, не содержит ошибок, таких как чтение, за пределами длины массивов. В идеале это все, что вам нужно, и вам не нужно больше ничего делать. Если вам захочется сделать что-то и / или если вы действительно наблюдаете проблемы с производительностью, я могу предложить две идеи:

Использование TypeScript может помочь , Большое полное предупреждение: типы TypeScript нацелены на производительность разработчика, а не на производительность выполнения (и, как выясняется, эти две точки зрения предъявляют очень разные требования к системе типов). Тем не менее, есть некоторое перекрытие: например, если вы последовательно аннотируете вещи как number, то компилятор TS предупредит вас, если вы случайно поместите null в массив или функцию, которая должна содержать / работать только с числами. Конечно, дисциплина все еще необходима: один number_func(random_object as number) аварийный люк может молча подорвать все, потому что правильность аннотаций типов нигде не соблюдается.

Использование TypedArrays также может помочь. Они имеют немного больше накладных расходов (потребление памяти и скорость выделения) для каждого массива по сравнению с обычными JavaScript массивами (поэтому, если вам нужно много маленьких массивов, то обычные массивы, вероятно, более эффективны), и они менее гибки, потому что могут ' t растут или уменьшаются после выделения, но они дают гарантию, что все элементы имеют ровно один тип.

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

Нет, и это намеренно. Как объяснялось выше, мы не хотим, чтобы вы специально адаптировали свой код к тем шаблонам, которые V8 может оптимизировать особенно хорошо сегодня, и мы не верим, что вы действительно хотите это сделать. Этот набор вещей может измениться в любом направлении: если есть шаблон, который вы хотели бы использовать, мы могли бы оптимизировать его для будущей версии (ранее мы играли с идеей хранения неупакованных 32-разрядных целых чисел в качестве элементов массива). ... но работа над этим еще не началась, так что никаких обещаний); и иногда, если есть шаблон, который мы использовали для оптимизации в прошлом, мы могли бы отказаться от него, если он помешает другим, более важным / действенным оптимизациям. Кроме того, такие вещи, как встроенная эвристика, как известно, трудно понять правильно, поэтому принятие правильного встроенного решения в нужное время является областью текущих исследований и соответствующих изменений в поведении движка / компилятора; Это делает еще один случай, когда для всех (вы и нас) было бы неудачно, если бы вы потратили много времени на настройку своего кода, пока какой-то набор текущих версий браузера не примет примерно те решения, которые вы думаете (или знаете). ?) лучше всего, только вернуться через полгода, чтобы понять, что тогдашние браузеры изменили свою эвристику.

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

...