Почему переназначение указателя на функцию замедляет вызов функции - PullRequest
2 голосов
/ 30 мая 2019

Я использовал указатель функции, p, для вызова различных функций. Моя проблема в том, что производительность вызова p резко снижается, если я указываю на другую функцию после того, как ей уже была назначена функция. Производительность хороша, если я переключаю p между undefined и той же функцией снова и снова, и хороша, когда я указываю только одну функцию, но переключение между функциями снижает производительность.

Ниже приведен код, который я использую для проверки этого сценария, а здесь - скрипка . Я рекурсивно повторяю 500 раз и вызываю p 1 000 000 раз в каждом цикле. p может быть undefined или указывать на func1 или func2.

function func1() {} // two identical empty functions
function func2() {} // different in name only

var p = func1; // default to func1

var count  = 0; // current loop
var elapse = 0; // elapsed time for 1,000,000 calls on each loop
var start  = 0; // start time for 1,000,000 calls on each loop
var total  = 0; // total elapsed time for all loops

function loop() {

  start = performance.now(); // get start time

  for (let i = 0; i < 1000000; i ++) if (p !== undefined) p(); // do 1,000,000 calls or early out 1,000,000 times if undefined

    elapse = performance.now() - start;

  total += elapse; // used for getting average

  count ++;

  console.log(p + "\nelapsed " + elapse + "\naverage " + total / count);

  // Switch between the lines below to see the performance difference.
  p = (p === func1) ? p = undefined : p = func1; // best performance
  //p = (p === func1) ? p = func1 : p = func1; // good performance
  //p = (p === func1) ? p = func2 : p = func1; // bad performance

  // pattern: func1 -> undefined -> func2 -> undefined -> repeat
  /*if (p === undefined) p = (count % 4 === 0) ? p = func1 : p = func2;
  else p = undefined;*/ // also bad performance

  if (count < 500) loop(); // start the next loop

}

console.clear();

loop(); // start the loop

Почему производительность вызова p значительно падает, когда ему назначается другая функция? Кроме того, почему установка p на undefined и возврат к исходной функции не изменяет производительность, если настройка p на undefined, а затем на другую функцию?

Ответы [ 2 ]

0 голосов
/ 31 мая 2019

Даже если я вызову p один раз, когда он указывает на func1, а затем назначу его func2 и вызову его еще раз до запуска цикла, в Chrome все равно будет потеря производительности примерно на 2,5 мс. Если кеш был сброшен, я не понимаю потери.

Ваша ментальная модель V8 не точна. Он может JIT-компилировать JS в машинный код в некоторых условиях, но все, что он не может обработать, «де-оптимизирует» всю функцию (или блок, или цикл?) И сделает ее интерпретируемой.

Я не большой эксперт по JS или V8, но я подобрал некоторые детали. Google нашел это: https://ponyfoo.com/articles/an-introduction-to-speculative-optimization-in-v8

Таким образом, это не значит, что вы один раз аннулируете «кеш», вы удаляете инвариантное условие, на основе которого оно оптимизировалось.


Предсказание ветвления как кеша для целей ветвления имеет значение, только когда вы переводите JS-среду выполнения в JIT на собственный машинный код вместо интерпретации. При интерпретации управляющие зависимости в JS - это просто зависимости данных в интерпретаторе, работающем на собственном ЦП.

Если удаление этого инварианта де-оптимизирует функцию или горячий цикл, то это ваша проблема, а не предсказание ветвления.

0 голосов
/ 30 мая 2019

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

См. Раздел «Конвейеры интерпретатора / компилятора в движках JavaScript» в этой статье: https://mathiasbynens.be/notes/shapes-ics

На рисунках показано, как TurboFan оптимизирует байт-код на основе данных профилирования при исполнении, и текст после него поясняет:

Для ускорения работы байт-код может быть отправлен оптимизирующему компилятору вместе с данными профилирования. Оптимизирующий компилятор делает определенные предположения на основе имеющихся данных профилирования, а затем создает высокооптимизированный машинный код.

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

Когда вы переназначаете указатель на функцию, вы отправляете противоречивые данные профилирования из интерпретатора в компилятор. Этого не происходит, когда вы назначаете undefined, потому что этот путь к коду не выполняется в этом случае: if (p !== undefined) p();

...