Performance: récursivité vs itération en Javascript

24

J'ai lu récemment quelques articles (par exemple http://dailyjs.com/2012/09/14/functional-programming/ ) sur les aspects fonctionnels de Javascript et la relation entre Scheme et Javascript (ce dernier a été influencé par le premier, qui est un langage fonctionnel, tandis que les aspects OO sont hérités de Self qui est un langage basé sur le prototypage).

Cependant ma question est plus précise: je me demandais s'il y avait des métriques sur les performances de la récursion vs l'itération en Javascript.

Je sais que dans certaines langues (où, par conception, l'itération fonctionne mieux), la différence est minime car l'interpréteur / compilateur convertit la récursivité en itération, mais je suppose que ce n'est probablement pas le cas de Javascript car il s'agit, au moins partiellement, d'une fonction la langue.

mastazi
la source
3
Faites votre propre test et découvrez tout de suite sur jsperf.com
TehShrike
avec la prime et une réponse mentionnant le TCO. Il semble que ES6 spécifie le TCO mais jusqu'à présent, personne ne le met en œuvre si nous pensons que kangax.github.io/compat-table/es6 Suis-je en train de manquer quelque chose?
Matthias Kauer

Réponses:

28

JavaScript n'effectue pas d'optimisation de la récursivité de queue , donc si votre récursivité est trop profonde, vous pouvez obtenir un débordement de pile d'appels. L'itération n'a pas de tels problèmes. Si vous pensez que vous allez trop récursif et que vous avez vraiment besoin d'une récursivité (par exemple, pour effectuer un remplissage par saturation), remplacez la récursivité par votre propre pile.

Les performances de récursivité sont probablement pires que les performances d'itération, car les appels et les retours de fonctions nécessitent une préservation et une restauration d'état, tandis que l'itération saute simplement à un autre point dans une fonction.

Triang3l
la source
Je me demandais juste ... J'ai vu un peu de code où un tableau vide est créé et le site de la fonction récursive est affecté à une position dans le tableau, puis la valeur stockée dans le tableau est renvoyée. Est-ce ce que vous entendez par "remplacer la récursivité par votre propre pile"? Par exemple: var stack = []; var factorial = function(n) { if(n === 0) { return 1 } else { stack[n-1] = n * factorial(n - 1); return stack[n-1]; } }
mastazi
@mastazi: Non, cela fera une pile d'appels inutiles avec la interne. Je voulais dire quelque chose comme le remplissage d'inondation basé sur la file d'attente de Wikipedia .
Triang3l
Il convient de noter qu'un langage n'effectue pas le TCO, mais qu'une mise en œuvre le pourrait. La façon dont les gens optimisent JS signifie que peut-être le TCO pourrait apparaître dans quelques implémentations
Daniel Gratzer
1
@mastazi Remplacez le elsedans cette fonction par else if (stack[n-1]) { return stack[n-1]; } elseet vous avez la mémorisation . Celui qui a écrit ce code factoriel avait une implémentation incomplète (et aurait probablement dû l'utiliser stack[n]partout au lieu de stack[n-1]).
Izkata
Merci @Izkata, je fais souvent ce genre d'optimisation mais jusqu'à aujourd'hui je ne connaissais pas son nom. J'aurais dû étudier le CS au lieu de l'informatique ;-)
mastazi
20

Mise à jour : depuis ES2015, JavaScript a un TCO , donc une partie de l'argument ci-dessous ne tient plus.


Bien que Javascript n'ait pas d'optimisation des appels de queue, la récursivité est souvent la meilleure façon de procéder. Et sincèrement, sauf dans les cas extrêmes, vous n'obtiendrez pas de débordements de pile d'appels.

Les performances sont quelque chose à garder à l'esprit, mais aussi une optimisation prématurée. Si vous pensez que la récursivité est plus élégante que l'itération, alors allez-y. S'il s'avère que c'est votre goulot d'étranglement (qui peut ne jamais l'être), vous pouvez le remplacer par une itération laide. Mais la plupart du temps, le goulot d'étranglement réside dans les manipulations DOM ou plus généralement les E / S, pas le code lui-même.

La récursivité est toujours plus élégante 1 .

1 : Opinion personnelle.

Florian Margaine
la source
3
Je conviens que la récursivité est plus élégante, et l'élégance est importante car c'est la lisibilité ainsi que la maintenabilité (c'est subjectif, mais à mon avis, la récursivité est très facile à lire, donc maintenable). Cependant, parfois les performances sont importantes; pouvez-vous soutenir l'affirmation selon laquelle la récursivité est la meilleure solution, même en termes de performances?
mastazi
3
@mastazi comme dit dans ma réponse, je doute que la récursivité soit votre goulot d'étranglement. La plupart du temps, c'est la manipulation DOM, ou plus généralement les E / S. Et n'oubliez pas que l'optimisation prématurée est à l'origine de tous les maux;)
Florian Margaine
+1 pour la manipulation DOM étant un goulot d'étranglement la plupart du temps! Je me souviens d'une interview très intéressante de Yehuda Katz (Ember.js) à ce sujet.
mastazi
1
@mike La définition de "prématuré" est "mûre ou mûre avant l'heure". Si vous savez que faire récursivement quelque chose provoquera un débordement de pile, ce n'est pas prématuré. Cependant, si vous supposez sur un coup de tête (sans aucune donnée réelle), alors c'est prématuré.
Zirak
2
Avec Javascript, vous ne savez pas combien de pile le programme aura à disposition. Vous pouvez avoir une petite pile dans IE6 ou une grosse pile dans FireFox. Les algorithmes récursifs ont rarement une profondeur fixe, sauf si vous faites une boucle récursive de style Scheme. Il ne semble tout simplement pas que la récursion non basée sur la boucle puisse éviter les optimisations prématurées.
mike30
7

J'étais également assez curieux de connaître ces performances en javascript, j'ai donc fait quelques expériences (bien que sur une ancienne version de node). J'ai écrit une calculatrice factorielle récursivement par rapport aux itérations et l'ai exécutée plusieurs fois localement. Le résultat semblait assez fortement biaisé vers une récursivité ayant une taxe (attendue).

Code: https://github.com/j03m/trickyQuestions/blob/master/factorial.js

Result:
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:557
Time:126
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:519
Time:120
j03m-MacBook-Air:trickyQuestions j03m$ node factorial.js 
Time:541
Time:123
j03m-MacBook-Air:trickyQuestions j03m$ node --version
v0.8.22
Dr.HappyPants
la source
Vous pouvez essayer cela avec "use strict";et voir si cela fait une différence. (Il produira des jumps au lieu de la séquence d'appel standard)
Burdock
1
Sur une version récente de node (6.9.1), j'ai obtenu des résultats extrêmement similaires. Il y a un peu de taxe sur la récursivité, mais je considère que c'est un cas extrême - la différence de 400 ms pour 1 000 000 de boucles est de 0,0025 ms par boucle. Si vous faites 1 000 000 de boucles, c'est quelque chose à garder à l'esprit.
Kelz
6

Selon la demande de l'OP, je participerai (sans me ridiculiser, j'espère: P)

Je pense que nous sommes tous d'accord pour dire que la récursivité est juste une façon plus élégante de coder. Si bien fait, cela peut rendre le code plus maintenable, ce qui est à mon humble avis tout aussi important (sinon plus) que le rasage de 0,0001 ms.

En ce qui concerne l'argument selon lequel JS n'effectue pas l'optimisation des appels de queue: ce n'est plus tout à fait vrai, l' utilisation du mode strict d'ECMA5 permet le TCO. C'était quelque chose dont je n'étais pas trop content il y a quelque temps, mais au moins je sais maintenant pourquoi arguments.calleejette des erreurs en mode strict. Je connais le lien ci-dessus qui renvoie à un rapport de bogue, mais le bogue est réglé sur WONTFIX. En outre, le TCO standard arrive: ECMA6 (décembre 2013).

Instinctivement, et fidèle à la nature fonctionnelle de JS, je dirais que la récursivité est le style de codage le plus efficace 99,99% du temps. Cependant, Florian Margaine a raison de dire qu'il est probable que le goulot d'étranglement se trouve ailleurs. Si vous manipulez le DOM, vous devez probablement vous concentrer sur l'écriture de votre code aussi maintenable que possible. L'API DOM est ce qu'elle est: lente.

Je pense qu'il est presque impossible d'offrir une réponse définitive quant à l'option la plus rapide. Dernièrement, beaucoup de jspref que j'ai vus montrent que le moteur V8 de Chrome est ridiculement rapide pour certaines tâches, qui fonctionnent 4 fois plus lentement sur SpiderMonkey de FF et vice versa. Les moteurs JS modernes ont toutes sortes de trucs dans leurs manches pour optimiser votre code. Je ne suis pas un expert, mais je pense que la V8, par exemple, est hautement optimisée pour les fermetures (et la récursivité), contrairement au moteur JScript de MS. SpiderMonkey fonctionne souvent mieux lorsque le DOM est impliqué ...

En bref: je dirais que la technique qui sera la plus performante est, comme toujours en JS, presque impossible à prévoir.

Elias Van Ootegem
la source
3

Sans mode strict, les performances d'itération sont généralement légèrement plus rapides que la récursivité ( en plus de faire travailler le JIT plus ). L'optimisation de la récursivité de queue élimine essentiellement toute différence notable car elle transforme toute la séquence d'appel en un saut.

Exemple: Jsperf

Je suggérerais de m'inquiéter beaucoup plus de la clarté et de la simplicité du code lorsqu'il s'agit de choisir entre la récursivité et l'itération.

Bardane
la source