web-dev-qa-db-fra.com

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 la fonction inversée fonctionne beaucoup plus rapidement. Au lieu de cela, un résultat étonnant est sorti.

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

Exemple:

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

Même comportement lors d'un test dans un ordre alternatif.

> 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 de pourquoi cela se produirait. L'effet dure jusqu'à ce que je rafraîchisse la page actuelle ou redémarre Node REPL.

Quelle pourrait être une source de performances aussi importantes (~ 12 fois pires)?

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

Les miens étaient:

Système d'exploitation: Ubuntu 14.04
Noeud v0.10.37
Chrome 43.0.2357.134 (version officielle) (64 bits)

/Éditer
Sur Firefox 39, il faut environ 5500 ms pour chaque test, quel que soit l'ordre. Il semble ne se produire que sur des moteurs spécifiques.

/ Edit2
En alignant la fonction sur la fonction de test, elle s'exécute toujours en même temps.
Est-il possible qu'il y ait une optimisation qui insère le paramètre de fonction si c'est toujours la même fonction?

64
Krzysztof Wende

Une fois que vous appelez test avec deux fonctions différentes fn() le site d'appel à l'intérieur devient mégamorphique et V8 ne peut pas s'y aligner.

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

Étant donné que V8 n'est pas en mesure de s'aligner sur le site d'appel fn(), il ne peut pas appliquer diverses optimisations à votre code. Si vous regardez votre code dans IRHydra (j'ai téléchargé des artefacts de compilation sur Gist pour votre convenance), vous remarquerez que la première version optimisée de test (quand elle était spécialisée pour fn = straight) a une boucle principale complètement vide.

enter image description here

V8 vient de mettre en ligne straight et a supprimé tout le code que vous espériez comparer avec l'optimisation Dead Code Elimination. Sur une version plus ancienne de V8 au lieu de DCE, V8 ne ferait que sortir le code de la boucle via LICM - car le code est complètement invariant en boucle.

Lorsque straight n'est pas en ligne, V8 ne peut pas appliquer ces optimisations - d'où la différence de performances. Une version plus récente de V8 appliquerait toujours DCE aux straight et inversed eux-mêmes en les transformant en fonctions vides

enter image description here

donc la différence de performance n'est pas si grande (environ 2-3x). Le V8 plus ancien n'était pas assez agressif avec DCE - et cela se manifesterait par une plus grande différence entre les cas en ligne et non en ligne, car les performances maximales du cas en ligne étaient uniquement le résultat d'un mouvement de code agressif en boucle invariante (LICM).

Sur une note connexe, cela montre pourquoi les repères ne devraient jamais être écrits comme cela - car leurs résultats ne sont d'aucune utilité lorsque vous finissez par mesurer une boucle vide.

Si vous êtes intéressé par le polymorphisme et ses implications dans la V8, consultez mon article "Quoi de neuf avec le monomorphisme" (la section "Tous les caches ne sont pas les mêmes" parle des caches associés aux appels de fonction). Je recommande également de lire l'un de mes exposés sur les dangers de la micro-analyse comparative, par exemple le plus récent "Benchmarking JS" exposé de GOTO Chicago 2015 ( vidéo ) - cela pourrait vous aider à éviter les pièges courants.

102
Vyacheslav Egorov

Vous comprenez mal la pile.

Alors que la "vraie" pile 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, tant que vous avez son adresse. Cela signifie que l'ordre des sections locales n'a pas d'importance, même si le compilateur ne le réorganise pas pour vous. En pseudo-Assemblée, 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 vrai code ressemble plus à ceci:

Push 1 ; x
Push 2 ; y

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

Si la pile de threads était vraiment une pile réelle et stricte, ce serait impossible, c'est vrai. Dans ce cas, l'ordre des opérations et des sections locales importerait beaucoup plus qu'aujourd'hui. Au lieu de cela, en autorisant l'accès aléatoire aux valeurs de la pile, vous économisez beaucoup de travail à la fois pour les compilateurs et le CPU.

Pour répondre à votre question, je soupçonne qu'aucune des fonctions ne fait quoi que ce soit. Vous ne modifiez que des sections locales et vos fonctions ne retournent rien - il est parfaitement légal que le compilateur supprime complètement les corps de fonction, et peut-être même les appels de fonction. Si c'est effectivement le cas, quelle que soit la différence de performances que vous observez est probablement juste un artefact de mesure, ou quelque chose lié aux coûts inhérents à l'appel d'une fonction/itération.

17
Luaan

En alignant la fonction sur la fonction de test, elle s'exécute toujours en même temps.
Est-il possible qu'il y ait une optimisation qui insère 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 de toute façon les corps de vos fonctions straight et inverse car ils n'ont aucun effet secondaire, mais ne manipulent que certaines variables locales.

Lorsque vous appelez test(…, 100000) pour la première fois, le compilateur d'optimisation se rend compte, après quelques itérations, que la fn() appelée est toujours la même et l'inline, évitant ainsi l'appel de fonction coûteux. Tout ce qu'il fait maintenant, c'est 10 millions de fois décrémenter une variable et la tester par rapport à 0.

Mais lorsque vous appelez test avec un autre fn, il doit alors être désoptimisé. Il peut faire plus tard d'autres optimisations, mais sachant maintenant qu'il y a deux fonctions différentes à appeler, il ne peut plus les aligner.

Étant donné que la seule chose que vous mesurez vraiment est l'appel de fonction, cela entraîne des différences graves dans vos résultats.

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

Concernant votre question réelle, non, les variables individuelles ne sont pas stockées sur une pile ( stack machine ), mais dans des registres ( register machine ). Peu importe l'ordre dans lequel ils sont déclarés ou utilisés dans votre fonction.

Pourtant, ils sont stockés sur la pile , dans le cadre de ce que l'on appelle les "cadres de pile". Vous aurez une trame 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]
…
3
Bergi