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


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

поэтому я написал небольшой тест производительности

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, так и в узле.js и я совершенно не понимаю, почему это произошло. Эффект длится до тех пор, пока я не обновлю текущую страницу или не перезапущу узел REPL.

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

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

мои были:

ОС: Ubuntu 14.04
Узел в v0.10.37
Chrome 43.0.2357.134 (официальная сборка) (64-бит)

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

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

3 64

3 ответа:

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

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

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

enter image description here

V8 просто встроенный straight и удалены весь код, который вы надеялись проверить с помощью оптимизации устранения мертвого кода. На более старой версии V8 вместо DCE V8 просто поднимет код из цикл через LICM-потому что код полностью инвариантен к циклу.

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

enter image description here

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

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

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

вы неправильно поняли стек.

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

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

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

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

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

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

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

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

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

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

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

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

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