Qu'est-ce qui rend cette fonction beaucoup plus lente?

J'ai essayé de faire une expérience pour voir si les variables locales dans les fonctions sont stockées sur une pile.

J'ai donc écrit un petit test de performance

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 } 

Je m'attendais à ce que les fonctions inversées fonctionnent beaucoup plus rapidement. Au lieu de cela, un résultat extraordinaire est sorti.

Jusqu'à ce que je teste une des fonctions, il fonctionne 10 fois plus vite qu'après avoir testé le second.

Exemple:

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

Le même comportement est testé dans un autre ordre.

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

Je l'ai testé à la fois dans le navigateur Chrome et dans Node.js et je n'ai absolument aucune idée pourquoi cela se produirait. L'effet dure jusqu'à ce que je rafraîchisse la page actuelle ou redéfinis Node REPL.

Quelle pourrait être une source de performance significative (~ 12 fois pire)?

PS. Puisqu'il semble fonctionner uniquement dans certains environnements, écrivez l'environnement que vous utilisez pour le tester.

Les miennes étaient:

OS: Ubuntu 14.04
Node v0.10.37
Chrome 43.0.2357.134 (version officielle) (64 bits)

/Modifier
Sur Firefox 39, il faut environ ~ 5500 ms pour chaque test indépendamment de la commande. Il semble se produire uniquement sur des moteurs spécifiques.

/ Edit2
L'insertion de la fonction dans la fonction de test la rend toujours en même temps.
Est-il possible qu'il y ait une optimisation qui inline le paramètre de fonction si c'est toujours la même fonction?

Une fois que vous appelez test avec deux fonctions différentes fn() callsite à l'intérieur devient megamorphique et V8 est incapable de l'intégrer.

Les appels de fonction (par opposition aux appels de méthode om(...) ) dans V8 sont accompagnés d' un cache en ligne d' un élément au lieu d'un véritable cache en ligne polymorphe.

Parce que V8 est incapable d'intégrer au site fn() il est impossible d'appliquer une variété d'optimisations à votre code. Si vous regardez votre code dans IRHydra (j'ai téléchargé des artefacts de compilation pour obtenir votre convenance), vous remarquerez que la première version optimisée du test (lorsqu'il était spécialisé pour fn = straight ) a une boucle principale complètement vide.

Entrez la description de l'image ici

V8 est juste en straight et supprime tout le code que vous espérez comparer avec Dead Code Elimination optimization. Sur une version antérieure de V8 au lieu de DCE V8, il suffit de lancer le code hors de la boucle via LICM – car le code est complètement invariant en boucle.

Lorsqu'il n'est pas en straight V8 ne peut pas appliquer ces optimisations, d'où la différence de performance. La version plus récente de V8 appliquerait toujours DCE à straight et inversed en les transformant en fonctions vides

Entrez la description de l'image ici

De sorte que la différence de performance n'est pas grande (environ 2-3x). Le V8 plus ancien n'a pas été assez agressif avec le DCE – et cela se manifeste par une différence plus importante entre les cas inline et non souligné, car la performance maximale du cas inlined résulte uniquement d'un mouvement de code invariant en boucle agressif (LICM).

En ce qui concerne la note, cela montre pourquoi les repères ne doivent jamais être écrit comme ceci – car leurs résultats ne sont pas utiles car vous finissez par mesurer une boucle vide.

Si vous êtes intéressé par le polymorphisme et ses implications dans V8, consultez mon article "Ce qu'il y a de monomorphisme" (section "Pas tous les caches sont identiques" parle des caches associées aux appels de fonctions). Je recommande également de lire un de mes discours sur les dangers du microbenchmarking, par exemple le plus récent "Benchmarking JS" de GOTO Chicago 2015 ( vidéo ) – cela pourrait vous aider à éviter les pièges courants.

Vous ne comprenez pas mal la pile.

Alors que la pile "réelle" n'a en effet que les opérations Push et Pop , cela ne s'applique pas vraiment au type de pile utilisé pour l'exécution. Outre Push et Pop , vous pouvez également accéder à n'importe quelle variable au hasard, aussi longtemps que vous avez son adresse. Cela signifie que l'ordre des locaux n'a pas d'importance, même si le compilateur ne le réorganise pas pour vous. En pseudo-assemblage, vous semblez penser que

 var x = 1; var y = 2; x = x + 1; y = y + 1; 

Se traduit par quelque chose comme

 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 vérité, le code réel est plus comme ceci:

 push 1 ; x push 2 ; y add [bp], 1 add [bp+4], 1 

Si la pile de fil était vraiment une pile réelle et stricte, cela serait impossible, vrai. Dans ce cas, l'ordre des opérations et des locaux importerait beaucoup plus qu'il ne le fait maintenant. Au lieu de cela, en permettant l'accès aléatoire aux valeurs sur la pile, vous économisez beaucoup de travail pour les compilateurs et la CPU.

Pour répondre à votre question actuelle, je ne pense pas que l'une ou l'autre des fonctions ne fait rien. Vous ne modifiez jamais les sections locales et vos fonctions ne renvoient rien – il est parfaitement légal que le compilateur supprime complètement les corps de fonction, et peut-être même les appels de fonctions. Si tel est le cas, quelle que soit la différence de performance que vous observez est probablement juste un artefact de mesure, ou quelque chose qui concerne les coûts inhérents à l'appel d'une fonction / itération.

L'insertion de la fonction dans la fonction de test la rend toujours en même temps.
Est-il possible qu'il y ait une optimisation qui inline le paramètre de fonction si c'est toujours la même fonction?

Oui, cela semble être exactement ce que vous observez. Comme déjà mentionné par @Luaan, le compilateur supprime probablement les corps de vos fonctions straight et inverse toute façon parce qu'ils n'ont pas d'effets secondaires, mais seulement manipulent certaines variables locales.

Lorsque vous appelez le test(…, 100000) pour la première fois, le compilateur d'optimisation se réalise après quelques itérations que le fn() appelé est toujours le même, et l'intègre, évitant l'appel coûteux. Tout ce qu'il fait maintenant est de 10 millions de fois en décrémentant une variable et en testant contre 0 .

Mais lorsque vous appelez le test avec un fn différent alors, il doit être désactivé. Il peut plus tard faire d'autres optimisations, mais maintenant, sachant qu'il y a deux fonctions différentes, on ne peut plus les enrayer.

Puisque la seule chose que vous mesurez vraiment, c'est l'appel de la fonction, qui mène à de graves différences dans vos résultats.

Une expérience pour voir si les variables locales dans les fonctions sont stockées sur une pile

En ce qui concerne votre question réelle, non, les variables individuelles ne sont pas stockées sur une pile ( pile ), mais dans des registres ( machine à enregistrer ). Ce n'est pas grave dans quel ordre ils sont déclarés ou utilisés dans votre fonction.

Pourtant, ils sont stockés sur la pile , dans le cadre des "blocs de pile". Vous aurez une image par appel de fonction, stockant les variables de son contexte d'exécution. Dans votre cas, la pile pourrait ressembler à ceci:

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