Что заставляет эту функцию работать намного медленнее?

Я пытаюсь провести эксперимент, чтобы увидеть, хранятся ли локальные переменные в функциях в стеке.

Итак, я написал небольшой тест производительности

function test(fn, times){
    var i = times;
    var t = Date.now()
    while(i--){
        fn()
    }
    return Date.now() - t;
} 
ene
function straight(){
    var a = 1
    var b = 2
    var c = 3
    var d = 4
    var e = 5
    a = a * 5
    b = Math.pow(b, 10)
    c = Math.pow(c, 11)
    d = Math.pow(d, 12)
    e = Math.pow(e, 25)
}
function inversed(){
    var a = 1
    var b = 2
    var c = 3
    var d = 4
    var e = 5
    e = Math.pow(e, 25)
    d = Math.pow(d, 12)
    c = Math.pow(c, 11)
    b = Math.pow(b, 10)
    a = a * 5
}

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

Пока я не проверю одну из функций, она работает в 10 раз быстрее, чем после тестирования второго.

Пример:

> test(straight, 10000000)
30
> test(straight, 10000000)
32
> test(inversed, 10000000)
390
> test(straight, 10000000)
392
> test(inversed, 10000000)
390

Такое же поведение при тестировании в альтернативном порядке.

> test(inversed, 10000000)
25
> test(straight, 10000000)
392
> test(inversed, 10000000)
394

Я тестировал его как в браузере Chrome, так и в Node.js, и я не знаю, почему это произойдет. Эффект сохраняется до обновления текущей страницы или перезапуска Node REPL.

Что может быть источником таких значительных (в 12 раз хуже) производительности?

PS. Поскольку он работает только в некоторых средах, напишите среду, которую вы используете для ее проверки.

Мои были:

ОС: Ubuntu 14.04
Node v0.10.37
Chrome 43.0.2357.134 (Official Build) (64-разрядная версия)

/Edit
В Firefox 39 для каждого теста требуется ~ 5500 мс независимо от порядка. Кажется, что это происходит только на определенных двигателях.

/Edit2
Включение функции в тестовую функцию заставляет ее работать всегда в одно и то же время.
Возможно ли, что существует оптимизация, которая строит параметр функции, если она всегда является одной и той же функцией?

Ответ 1

Как только вы вызываете test с двумя различными функциями fn() callsite внутри, он становится мегаморфным, а V8 не может подключаться к нему.

Функциональные вызовы (в отличие от вызовов методов o.m(...)) в V8 сопровождаются одним элементом встроенного кеша, а не истинным полиморфным встроенным кешем.

Поскольку V8 не может подключиться к fn() callsite, он не может применить к вашему коду различные оптимизации. Если вы посмотрите на свой код в IRHydra (я загрузил артефакты компиляции для вашего удобства), вы заметите, что первая оптимизированная версия test (когда он был специализирован для fn = straight) имеет полностью пустой основной цикл.

введите описание изображения здесь

V8 просто вложил straight и удалил весь код, который вы надеялись сравнить с оптимизацией Dead Code Elimination. В более старой версии V8 вместо DCE V8 будет просто вытаскивать код из цикла через LICM - потому что код полностью петлевый инвариант.

Когда straight не встраивается, V8 не может применять эти оптимизации - отсюда разница в производительности. Более новая версия V8 по-прежнему будет применять DCE к straight и inversed, превращая их в пустые функции

введите описание изображения здесь

поэтому разница в производительности не такая большая (около 2-3х). Старые V8 не были достаточно агрессивны с DCE, и это проявилось бы в большей разнице между inlined и not-inlined случаями, потому что максимальная производительность вложенного случая была исключительно результатом агрессивного цикла-инвариантного движения кода (LICM).

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

Если вы заинтересованы в полиморфизме и его последствиях в V8, посмотрите мой пост "Что с мономорфизмом" (раздел "Не все кеши те же" разговоры о кэшах, связанных с вызовами функций "). Я также рекомендую прочитать один из моих разговоров об опасностях микромаркетинга, например. последние " Benchmarking JS " беседуют с GOTO Chicago 2015 ( видео) - это может помочь вам избежать общих ошибок.

Ответ 2

Вы не понимаете стек.

В то время как "реальный" стек действительно имеет только операции Push и Pop, это действительно не относится к типу стека, используемого для выполнения. Помимо Push и Pop, вы также можете получить доступ к любой переменной произвольно, если у вас есть свой адрес. Это означает, что порядок локальных пользователей не имеет значения, даже если компилятор не переупорядочивает его для вас. В псевдосборке вам кажется, что

var x = 1;
var y = 2;

x = x + 1;
y = y + 1;

переводит на что-то вроде

push 1 ; x
push 2 ; y

; get y and save it
pop tmp
; get x and put it in the accumulator
pop a
; add 1 to the accumulator
add a, 1
; store the accumulator back in x
push a
; restore y
push tmp
; ... and add 1 to y

По правде говоря, реальный код выглядит примерно так:

push 1 ; x
push 2 ; y

add [bp], 1
add [bp+4], 1

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

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

Ответ 3

Вложение функции в тестовую функцию заставляет ее работать всегда в одно и то же время.
Возможно ли, что существует оптимизация, которая строит параметр функции, если она всегда является одной и той же функцией?

Да, это похоже на то, что вы наблюдаете. Как уже упоминалось в @Luaan, компилятор, вероятно, в любом случае сбрасывает тела ваших функций straight и inverse, потому что у них нет каких-либо побочных эффектов, а работает только с некоторыми локальными переменными.

Когда вы вызываете test(…, 100000) в первый раз, оптимизирующий компилятор реализует после некоторых итераций, что вызываемый fn() всегда один и тот же, и делает его встроенным, избегая дорогостоящего вызова функции. Все, что он делает сейчас, составляет 10 миллионов раз, уменьшая переменную и тестируя ее против 0.

Но когда вы вызываете test с другим fn, тогда он должен де-оптимизировать. Позднее он может сделать некоторые другие оптимизации, но теперь, зная, что есть две разные функции, которые можно назвать, они больше не могут их встроить.

Поскольку единственное, что вы действительно измеряете, это вызов функции, что приводит к серьезным различиям в ваших результатах.

Эксперимент, чтобы проверить, хранятся ли локальные переменные в функциях в стеке

Что касается вашего фактического вопроса, нет, отдельные переменные не хранятся в стеке (стековый компьютер), но в регистре (регистрировать машину). Не имеет значения, в каком порядке они объявлены или используются в вашей функции.

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

[straight: a, b, c, d, e]
[test: fn, times, i, t]
…