Ответ 1
Во втором случае +
выполняется быстрее, потому что в этом случае V8 фактически вытесняет его из цикла бенчмаркинга - делает цикл сравнения пустым.
Это происходит из-за некоторых особенностей текущего оптимизационного конвейера. Но прежде чем мы перейдем к деталям gory, я хотел бы напомнить, как работает Benchmark.js.
Чтобы измерить тестовый сценарий, который вы написали, он принимает Benchmark.prototype.setup
, который вы также предоставили, и сам тест, и динамически генерирует функцию, которая выглядит приблизительно как это (я пропускаю некоторые нерелевантные детали):
function (n) {
var start = Date.now();
/* Benchmark.prototype.setup body here */
while (n--) {
/* test body here */
}
return Date.now() - start;
}
После создания функции Benchmark.js вызывает ее для измерения вашего op для определенного количества итераций n
. Этот процесс повторяется несколько раз: сгенерируйте новую функцию, вызовите ее, чтобы собрать образец измерения. Количество итераций настраивается между выборками, чтобы гарантировать, что функция работает достаточно долго, чтобы дать осмысленное измерение.
Важно отметить, что
- и ваш случай, и
Benchmark.prototype.setup
являются текстовыми. - есть цикл вокруг операции, которую вы хотите измерить;
По существу, мы обсуждаем, почему приведенный ниже код с локальной переменной x
function f(n) {
var start = Date.now();
var x = "5555"
while (n--) {
var y = +x
}
return Date.now() - start;
}
работает медленнее, чем код с глобальной переменной x
function g(n) {
var start = Date.now();
x = "5555"
while (n--) {
var y = +x
}
return Date.now() - start;
}
(Примечание: этот случай называется локальной переменной в самом вопросе, но это не тот случай, x
является глобальным)
Что происходит, когда вы выполняете эти функции с достаточно большими значениями n
, например f(1e6)
?
Текущий трубопровод оптимизации реализует OSR особым образом. Вместо того, чтобы генерировать определенную версию оптимизированного кода OSR и отбрасывать его позже, он генерирует версию, которая может использоваться как для OSR, так и для нормальной записи, и может даже использоваться повторно, если нам нужно выполнить OSR в том же цикле. Это делается путем ввода специального блока ввода OSR в нужное место на графике потока управления.
Блок ввода OSR вводится, а SSA IR для функции встроен и он скопирует все локальные переменные из входящего состояния OSR. В результате V8 не видит, что локальный x
фактически является константой и даже теряет всякую информацию о его типе. Для последующих проходов оптимизации x2
выглядит так, как будто это может быть что угодно.
В качестве x2
может быть любое выражение +x2
также может иметь произвольные побочные эффекты (например, это может быть объект с valueOf
, прикрепленный к нему). Это предотвращает движение цикла с инвариантным кодом из перемещения +x2
из цикла.
Почему g
быстрее, чем? V8 тянет трюк здесь. Он отслеживает глобальные переменные, которые содержат константы: например. в этом тесте global x
всегда содержит "5555"
, поэтому V8 просто заменяет доступ x
своим значением и помещает этот оптимизированный код в зависимость от значения x
. Если кто-то заменяет значение x
чем-то другим, чем все зависимые коды, будет деоптимизирован. Глобальные переменные также не являются частью состояния OSR и не участвуют в переименовании SSA, поэтому V8 не путается "ложными" φ-функциями, объединяющими OSR и нормальные состояния записи. Поэтому, когда V8 оптимизирует g
, он заканчивает создание следующего IR в теле цикла (красная полоса слева показывает цикл):
Примечание: +x
скомпилирован в x * 1
, но это всего лишь деталь реализации.
Позже LICM просто возьмет эту операцию и вытащит ее из цикла, не оставляя ничего интересного в самом цикле. Это становится возможным, поскольку теперь V8 знает, что оба операнда *
являются примитивами, поэтому могут быть не побочные эффекты.
И поэтому g
работает быстрее, потому что пустой цикл, очевидно, быстрее, чем непустое.
Это также означает, что вторая версия контрольного показателя фактически не измеряет то, что вы хотели бы измерить, и хотя первая версия действительно понимала некоторые различия между производительностью parseInt(x)
и +x
, которая была больше удачей: вы нанесли ограничение в текущем оптимизационном трубопроводе V8 (коленчатый вал), который помешал ему съесть весь микрообъект.