Подтвердить что ты не робот

JsPerf: преобразование ParseInt vs Plus

Я пытаюсь проверить, что преобразование плюс (+) выполняется быстрее, чем parseInt со следующим jsperf, и результаты меня удивили:

Parse vs Plus

Код подготовки

<script>
  Benchmark.prototype.setup = function() {
    var x = "5555";
  };
</script>

Пример анализа

var y = parseInt(x); //<---80 million loops

Пример плюс

var y = +x; //<--- 33 million loops

Причина в том, что я использую "Benchmark.prototype.setup", чтобы объявить свою переменную, но я не понимаю, почему

См. второй пример:

Parse vs Plus (локальная переменная)

<script>
  Benchmark.prototype.setup = function() {
    x = "5555";
  };
</script>

Пример анализа

var y = parseInt(x); //<---89 million loops

Пример плюс

var y = +x; //<--- 633 million loops

Может кто-нибудь объяснить результаты?

Спасибо

4b9b3361

Ответ 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 version of the control flow graph

Блок ввода 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 в теле цикла (красная полоса слева показывает цикл):

IR before LICM

Примечание: +x скомпилирован в x * 1, но это всего лишь деталь реализации.

Позже LICM просто возьмет эту операцию и вытащит ее из цикла, не оставляя ничего интересного в самом цикле. Это становится возможным, поскольку теперь V8 знает, что оба операнда * являются примитивами, поэтому могут быть не побочные эффекты.

IR after LICM

И поэтому g работает быстрее, потому что пустой цикл, очевидно, быстрее, чем непустое.

Это также означает, что вторая версия контрольного показателя фактически не измеряет то, что вы хотели бы измерить, и хотя первая версия действительно понимала некоторые различия между производительностью parseInt(x) и +x, которая была больше удачей: вы нанесли ограничение в текущем оптимизационном трубопроводе V8 (коленчатый вал), который помешал ему съесть весь микрообъект.

Ответ 2

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

var width = parseInt(element.style.width);//return width as integer

в то время как знак плюса не мог справиться с этим случаем:

var width = +element.style.width;//returns NaN

Знак плюса делает неявное преобразование из строки в число и только это преобразование. parseInt сначала пытается понять смысл строки (например, целые числа, помеченные измерением).