Разработчик V8 здесь. Учитывая интерес к этому вопросу и отсутствие других ответов, я могу попробовать; Боюсь, это будет не тот ответ, на который вы надеялись.
Есть ли где-нибудь набор рекомендаций о том, как программировать, оставаясь в мире упакованных массивов SMI (например)?
Краткий ответ: это прямо здесь: const guidelines = ["keep your integers small enough"]
.
Более развернутый ответ: дать исчерпывающий набор рекомендаций сложно по разным причинам. В целом, мы считаем, что разработчики JavaScript должны писать код, который имеет смысл для них и их варианта использования, а разработчики движка JavaScript должны выяснить, как быстро запускать этот код на своих движках. С другой стороны, очевидно, что у этого идеала есть некоторые ограничения в том смысле, что некоторые шаблоны кодирования всегда будут иметь более высокие затраты на производительность, чем другие, независимо от выбора реализации движка и усилий по оптимизации.
Когда мы говорим о рекомендациях по производительности, мы стараемся помнить об этом и тщательно оцениваем, какие рекомендации с высокой вероятностью останутся актуальными для многих движков и в течение многих лет, а также являются достаточно идиоматическими/ненавязчивыми.
Возвращаясь к рассматриваемому примеру: предполагается, что внутреннее использование Smis является деталью реализации, о которой код пользователя не должен знать. В одних случаях это сделает работу более эффективной, а в других не повредит. Не все движки используют Smis (например, AFAIK Firefox/Spidermonkey исторически не использовал его; я слышал, что в некоторых случаях они используют Smis в наши дни; но я не знаю никаких подробностей и не могу говорить с кем-либо об этом. причина). В V8 размер Smis является внутренней деталью и фактически менялся со временем и в различных версиях. На 32-битных платформах, которые раньше использовались в большинстве случаев, Smi всегда были 31-битными целыми числами со знаком; на 64-битных платформах они были 32-битными целыми числами со знаком, что недавно казалось наиболее распространенным случаем, пока в Chrome 80 мы не добавили «сжатие указателя» для 64-битных архитектур, что потребовало снижения размера Smi до известного 31 бита. с 32-битных платформ. Если бы вы построили реализацию на предположении, что Smis обычно 32-битные, вы бы столкнулись с такими неприятными ситуациями, как это.
К счастью, как вы заметили, двойные массивы по-прежнему очень быстры. Для числового кода, вероятно, имеет смысл предполагать/нацеливаться на двойные массивы. Учитывая преобладание двойников в JavaScript, разумно предположить, что все движки хорошо поддерживают двойники и двойные массивы.
Можно ли выполнять общее высокопроизводительное программирование на Javascript без использования чего-то вроде системы макросов для встраивания таких вещей, как vec.add() в callsites?
«общий» обычно расходится с «высокопроизводительным». Это не связано с JavaScript или конкретными реализациями движка.
«Универсальный» код означает, что решения должны приниматься во время выполнения. Каждый раз, когда вы выполняете функцию, должен запускаться код, чтобы определить, скажем, «является ли x
целым числом? Если да, выберите этот путь кода. Является ли x
строкой? Затем перепрыгните сюда. Это объект? Нет? Тогда, может быть, .toString()
? Может быть, в его цепочке прототипов? Вызовите это и перезапустите с самого начала с его результатом». «Высокопроизводительный» оптимизированный код, по сути, построен на идее отказа от всех этих динамических проверок; это возможно только тогда, когда у движка/компилятора есть какой-то способ вывести типы заранее: если он может доказать (или предположить с достаточно высокой вероятностью), что x
всегда будет целым числом, то ему нужно только сгенерировать код для этого случая (охраняется проверкой типа, если были задействованы недоказанные предположения).
Инлайнинг ортогонален всему этому. «Общая» функция все еще может быть встроена. В некоторых случаях компилятор может передавать информацию о типе во встроенную функцию, чтобы уменьшить там полиморфизм.
(Для сравнения: C++, будучи статически компилируемым языком, имеет шаблоны для решения связанной с этим проблемы. Короче говоря, они позволяют программисту явно указывать компилятору создавать специализированные копии функций (или целых классов), параметризованных для заданных типов. хорошее решение для некоторых случаев, но не без собственного набора недостатков, например, долгое время компиляции и большие двоичные файлы. JavaScript, конечно, не имеет такого понятия, как шаблоны. Вы могли бы использовать eval
для создания системы, которая чем-то похожа, но тогда вы столкнетесь с аналогичными недостатками: вам придется выполнять работу, эквивалентную компилятору C++, во время выполнения, и вам придется беспокоиться об огромном количестве кода, который вы создаете.)
Как можно объединить высокопроизводительный код в библиотеки в свете таких вещей, как мегаморфные сайты вызовов и деоптимизация? Например, если я с удовольствием использую пакет A линейной алгебры на высокой скорости, а затем я импортирую пакет B, который зависит от A, но B вызывает его с другими типами и деоптимизирует его, внезапно (без изменения моего кода) мой код работает медленнее. .
Да, это общая проблема с JavaScript. V8 использовался для внутренней реализации некоторых встроенных функций (таких как Array.sort
) в JavaScript, и эта проблема (которую мы называем «загрязнение обратной связи по типу») была одной из основных причин, по которой мы полностью отказались от этой техники.
Тем не менее, для числового кода не так уж много типов (только Smis и double), и, как вы заметили, на практике они должны иметь одинаковую производительность, поэтому, хотя загрязнение обратной связи типа действительно является теоретической проблемой, а в некоторых случаях может имеют значительное влияние, также вполне вероятно, что в сценариях линейной алгебры вы не увидите измеримой разницы.
Кроме того, внутри движка существует гораздо больше ситуаций, чем «один тип == быстро» и «более одного типа == медленно». Если данная операция видела и Smis, и двойников, это совершенно нормально. Загрузка элементов из двух видов массивов тоже подходит. Мы используем термин «мегаморфный» для ситуации, когда нагрузка имеет так много разных типов, что отказывается от отслеживания их по отдельности и вместо этого использует более общий механизм, который лучше масштабируется для большого количества типов — функция, содержащая такие нагрузки, может еще оптимизировать. «Деоптимизация» — это очень специфическое действие, когда приходится отбрасывать оптимизированный код для функции, потому что виден новый тип, которого раньше не было, и поэтому оптимизированный код не приспособлен для обработки. Но даже это нормально: просто вернитесь к неоптимизированному коду, чтобы собрать больше отзывов о типах, и снова оптимизируйте позже. Если это произойдет пару раз, то беспокоиться не о чем; это становится проблемой только в патологически тяжелых случаях.
Итак, резюме всего, что есть: не беспокойтесь об этом. Просто напишите разумный код, пусть с этим справится движок. И под «разумным» я подразумеваю: то, что имеет смысл для вашего варианта использования, удобочитаемо, удобно в сопровождении, использует эффективные алгоритмы, не содержит ошибок, таких как чтение за пределами длины массивов. В идеале это все, что нужно, и вам не нужно ничего делать. Если вы чувствуете себя лучше, делая что-то, и/или если вы действительно наблюдаете проблемы с производительностью, я могу предложить две идеи:
Использование TypeScript может помочь. Большое жирное предупреждение: типы TypeScript нацелены на производительность разработчиков, а не на производительность выполнения (и, как выясняется, эти две точки зрения имеют очень разные требования к системе типов). Тем не менее, есть некоторое совпадение: например. если вы постоянно аннотируете вещи как number
, то компилятор TS предупредит вас, если вы случайно поместите null
в массив или функцию, которая должна содержать/работать только с числами. Дисциплина, конечно, все равно нужна: один-единственный number_func(random_object as number)
аварийный люк может бесшумно подорвать все, ведь правильность аннотаций типов нигде не соблюдается.
Использование TypedArrays также может помочь. У них немного больше накладных расходов (потребление памяти и скорость выделения) на массив по сравнению с обычными массивами JavaScript (поэтому, если вам нужно много небольших массивов, то обычные массивы, вероятно, более эффективны), и они менее гибки, потому что они не могут расти. или уменьшаться после выделения, но они гарантируют, что все элементы имеют ровно один тип.
Существуют ли какие-либо хорошие простые в использовании инструменты измерения для проверки того, что движок Javascript делает внутри с типами?
Нет, и это намеренно. Как объяснялось выше, мы не хотим, чтобы вы специально адаптировали свой код к шаблонам, которые V8 сегодня может особенно хорошо оптимизировать, и мы не верим, что вы действительно хотите это делать. Этот набор вещей может измениться в любом направлении: если есть шаблон, который вы хотели бы использовать, мы можем оптимизировать его в будущей версии (ранее мы играли с идеей хранения неупакованных 32-битных целых чисел в качестве элементов массива). .. но работа над этим еще не началась, так что ничего не обещаю); и иногда, если есть шаблон, который мы использовали для оптимизации в прошлом, мы можем решить отказаться от него, если он мешает другим, более важным/эффективным оптимизациям. Кроме того, общеизвестно, что такие вещи, как эвристики встраивания, трудно реализовать правильно, поэтому принятие правильного решения о встраивании в нужное время является областью постоянных исследований и соответствующих изменений в поведении движка/компилятора; что делает это еще одним случаем, когда для всех (вас и нас) было бы неудачно, если бы вы потратили много времени на настройку своего кода до тех пор, пока какой-то набор текущих версий браузера не примет решения о встраивании, которые вы думаете (или знаете?) лучше всего, чтобы вернуться через полгода и понять, что современные браузеры изменили свою эвристику.
Конечно, вы всегда можете измерить производительность вашего приложения в целом — это то, что в конечном счете имеет значение, а не то, какой выбор конкретно сделал внутри движка. Остерегайтесь микротестов, поскольку они вводят в заблуждение: если вы извлечете только две строки кода и сравните их, то есть вероятность, что сценарий будет достаточно другим (например, отзывы другого типа), и движок примет совсем другие решения.
person
jmrk
schedule
11.03.2020
|0
за каждой операцией. Это некрасиво, но это лучшее, что вы можете сделать на языке, в котором нет правильных целых чисел. Вы также можете использовать BigInts, но на сегодняшний день они не очень быстры ни в одном из распространенных движков (в основном из-за отсутствия спроса). - person jmrk   schedule 12.03.2020