¿Qué hace que esta función funcione mucho más lento?


He estado tratando de hacer un experimento para ver si las variables locales en las funciones se almacenan en una pila.

Así que escribí una pequeña prueba de rendimiento

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
}

Esperaba que la función inversa funcionara mucho más rápido. En cambio, un resultado increíble salió.

Hasta que pruebe una de las funciones, se ejecuta 10 veces más rápido que después de probar la segunda.

Ejemplo:

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

El mismo comportamiento cuando se prueba en orden alternativo.

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

Lo he probado tanto en el navegador Chrome como en el nodo.js y yo no tenemos ni idea de por qué iba a pasar. El efecto dura hasta que refresco la página actual o reinicio Node REPL.

¿Qué podría ser una fuente de tan significativo (~12 veces peor) rendimiento?

PS. Dado que parece funcionar solo en algunos entornos, escriba el entorno que está utilizando para probarlo.

Los míos eran:

OS: Ubuntu 14.04
Nodo v0.10.37
Chrome 43.0.2357.134 (Versión oficial) (64 bits)

/ Editar
En Firefox 39 se tarda ~5500 ms para cada prueba, independientemente del orden. Parece ocurrir solo en motores específicos.

/Edit2
Incrustar la función en la función de prueba hace que se ejecute siempre al mismo tiempo.
¿Es posible que haya una optimización que alinee el parámetro de función si siempre es la misma función?

Author: Krzysztof Wende, 2015-07-29

3 answers

Una vez que llamas a test con dos funciones diferentes fn() callsite dentro se vuelve megamórfico y V8 no puede insertarlo.

Las llamadas a funciones (a diferencia de las llamadas a métodos o.m(...)) en V8 están acompañadas por un elemento caché en línea en lugar de una verdadera caché en línea polimórfica.

Debido a que V8 no puede insertar en fn() callsite, no puede aplicar una variedad de optimizaciones a su código. Si miras tu código en IRHydra (subí artefactos de compilación a gist para su conveniencia) notará que la primera versión optimizada de test (cuando se especializó para fn = straight) tiene un bucle principal completamente vacío.

introduzca la descripción de la imagen aquí

V8 acaba de inlinear straight y eliminado todo el código que esperaba comparar con la optimización de Eliminación de Código Muerto. En una versión anterior de V8 en lugar de DCE, V8 simplemente levantaría el código fuera del bucle a través de LICM, porque el código es completamente loop invariable.

Cuando straight no está en línea V8 no puede aplicar estas optimizaciones - de ahí la diferencia de rendimiento. La versión más reciente de V8 todavía aplicaría DCE a straight y inversed convirtiéndolos en funciones vacías

introduzca la descripción de la imagen aquí

Así que la diferencia de rendimiento no es tan grande (alrededor de 2-3x). Mayores V8 no fue lo suficientemente agresivo con DCE - y que se manifiesta en mayor diferencia entre entre líneas y entre líneas de los casos, debido a que el pico de rendimiento de línea el caso fue únicamente el resultado del movimiento de código invariante de bucle agresivo (LICM).

En una nota relacionada, esto muestra por qué los puntos de referencia nunca deben escribirse de esta manera, ya que sus resultados no son de ninguna utilidad, ya que termina midiendo un bucle vacío.

Si estás interesado en el polimorfismo y sus implicaciones en V8, echa un vistazo a mi post "Qué pasa con el monomorfismo" (la sección "No todos los cachés son iguales" habla sobre los cachés asociados con las llamadas a funciones). También recomiendo leer a través de una de mis charlas sobre los peligros del microbenchmarking, por ejemplo, la más reciente "Benchmarking JS" charla de GOTO Chicago 2015 (video), podría ayudarlo a evitar errores comunes.

 100
Author: Vyacheslav Egorov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 13:07:34

Estás malinterpretando la pila.

Mientras que la pila "real" de hecho solo tiene las operaciones Push y Pop, esto realmente no se aplica al tipo de pila utilizada para la ejecución. Además de Push y Pop, también puede acceder a cualquier variable al azar, siempre y cuando tenga su dirección. Esto significa que el orden de los locales no importa, incluso si el compilador no lo reordena por usted. En pseudo-asamblea, usted parece pensar que

var x = 1;
var y = 2;

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

Se traduce en algo como

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

En verdad, el código real es más como esto:

push 1 ; x
push 2 ; y

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

Si la pila de subprocesos realmente fuera una pila real y estricta, esto sería imposible, verdad. En ese caso, el orden de las operaciones y lugareños importa mucho más que ahora. En su lugar, al permitir el acceso aleatorio a los valores en la pila, se ahorra mucho trabajo tanto para los compiladores como para la CPU.

Para responder a su pregunta real, estoy sospechando que ninguna de las funciones realmente hace nada. Sólo eres siempre modificando locales, y sus funciones no devuelven nada - es perfectamente legal que el compilador elimine por completo los cuerpos de las funciones, y posiblemente incluso las llamadas a las funciones. Si eso es así, cualquier diferencia de rendimiento que esté observando es probablemente solo un artefacto de medición, o algo relacionado con los costos inherentes de llamar a una función / iteración.

 17
Author: Luaan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 11:28:12

Incrustar la función en la función de prueba hace que se ejecute siempre al mismo tiempo.
¿Es posible que haya una optimización que alinee el parámetro de función si siempre es la misma función?

Sí, esto parece ser exactamente lo que estás observando. Como ya mencionó @Luaan, el compilador probablemente elimina los cuerpos de sus funciones straight y inverse de todos modos porque no están teniendo ningún efecto secundario, sino que solo manipulan algunas funciones locales variable.

Cuando se llama a test(…, 100000) por primera vez, el compilador optimizador se da cuenta después de algunas iteraciones que el fn() que se llama es siempre el mismo, y lo hace en línea, evitando la costosa llamada a la función. Todo lo que hace ahora es 10 millones de veces decrementar una variable y probarla contra 0.

Pero cuando estás llamando a test con un fn diferente entonces, tiene que des-optimizar. Más tarde puede hacer algunas otras optimizaciones de nuevo, pero ahora sabiendo que hay dos funciones diferentes que se llamarán ya no pueden insertarlas.

Dado que lo único que realmente estás midiendo es la llamada a la función, eso conduce a las graves diferencias en tus resultados.

Un experimento para ver si las variables locales en las funciones se almacenan en una pila

Con respecto a su pregunta real, no, las variables individuales no se almacenan en una pila ( stack machine), sino en registros ( register machine). No importa en qué orden se declaran o se utilizan en su función.

Sin embargo, se almacenan en la pila, como parte de los llamados "marcos de pila". Tendrá un fotograma por llamada a función, almacenando las variables de su contexto de ejecución. En su caso, la pila podría verse así:

[straight: a, b, c, d, e]
[test: fn, times, i, t]
…
 3
Author: Bergi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 13:03:18