414 votes

Efficacité de la programmation purement fonctionnelle

Quelqu'un sait-il quel est le pire ralentissement asymptotique possible qui peut se produire lorsque l'on programme de manière purement fonctionnelle et non impérative (c'est-à-dire en autorisant les effets de bord) ?

Clarification du commentaire de itowlson : existe-t-il un problème pour lequel l'algorithme non destructeur le plus connu est asymptotiquement pire que l'algorithme destructeur le plus connu, et si oui, de combien ?

6 votes

De même que pour la programmation impérative, quelle qu'elle soit.

3 votes

@jldupont : Pour retourner le résultat d'un calcul bien sûr. Il existe de nombreux programmes sans effet secondaire. Ils ne peuvent simplement pas faire grand chose d'autre que de calculer sur leur entrée. Mais c'est toujours utile.

25 votes

Je peux le rendre aussi mauvais que vous le souhaitez, en écrivant mal mon code fonctionnel ! *grin* Je pense que votre question est "existe-t-il un problème pour lequel l'algorithme non destructif le plus connu est asymptotiquement pire que l'algorithme destructif le plus connu, et si oui, de combien ?"... est-ce exact ?

553voto

Brian Campbell Points 101107

Selon Pippenger [1996] Lorsque l'on compare un système Lisp purement fonctionnel (et doté d'une sémantique d'évaluation stricte, et non paresseuse) à un système capable de modifier les données, un algorithme écrit pour le Lisp impur qui s'exécute en O( n ) peut être traduit en un algorithme dans le Lisp pur qui s'exécute en O( n journal n ) temps (sur la base des travaux de Ben-Amram et Galil [1992] sur la simulation de la mémoire à accès aléatoire en utilisant uniquement des pointeurs). Pippenger établit également qu'il existe des algorithmes pour lesquels c'est le mieux que l'on puisse faire ; il existe des problèmes qui sont O( n ) dans le système impur qui sont Ω( n journal n ) dans le système pur.

Il y a quelques mises en garde à faire au sujet de ce document. La plus importante est qu'il ne traite pas des langages fonctionnels paresseux, tels que Haskell. Bird, Jones et De Moor [1997]. démontrent que le problème construit par Pippenger peut être résolu dans un langage fonctionnel paresseux en O( n ), mais ils n'établissent pas (et pour autant que je sache, personne ne l'a fait) si un langage fonctionnel paresseux peut ou non résoudre tous les problèmes dans le même temps d'exécution asymptotique qu'un langage avec mutation.

Le problème construit par Pippenger requiert Ω( n journal n ) est spécifiquement construit pour atteindre ce résultat, et n'est pas nécessairement représentatif des problèmes pratiques du monde réel. Il y a quelques restrictions sur le problème qui sont un peu inattendues, mais nécessaires pour que la preuve fonctionne ; en particulier, le problème exige que les résultats soient calculés en ligne, sans pouvoir accéder à l'entrée future, et que l'entrée consiste en une séquence d'atomes d'un ensemble non limité d'atomes possibles, plutôt qu'un ensemble de taille fixe. De plus, l'article n'établit des résultats (borne inférieure) que pour un algorithme impur de temps d'exécution linéaire ; pour les problèmes qui nécessitent un temps d'exécution plus long, il est possible que le temps supplémentaire de O(log n ) observé dans le problème linéaire peut être "absorbé" dans le processus des opérations supplémentaires nécessaires aux algorithmes ayant des temps d'exécution plus importants. Ces clarifications et questions ouvertes sont explorées brièvement par Ben-Amram [1996] .

En pratique, de nombreux algorithmes peuvent être implémentés dans un langage purement fonctionnel avec la même efficacité que dans un langage avec des structures de données mutables. Pour une bonne référence sur les techniques à utiliser pour implémenter efficacement des structures de données purement fonctionnelles, voir Chris Okasaki, "Purely Functional Data Structures" [Okasaki 1998]. (qui est une version étendue de sa thèse [Okasaki 1996] ).

Tous ceux qui ont besoin de mettre en œuvre des algorithmes sur des structures de données purement fonctionnelles devraient lire Okasaki. Vous pouvez toujours obtenir au pire un résultat O(log n ) par opération en simulant une mémoire mutable avec un arbre binaire équilibré, mais dans de nombreux cas, vous pouvez faire beaucoup mieux que cela, et Okasaki décrit de nombreuses techniques utiles, des techniques amorties aux techniques en temps réel qui effectuent le travail amorti de manière incrémentielle. Les structures de données purement fonctionnelles peuvent être un peu difficiles à utiliser et à analyser, mais elles offrent de nombreux avantages, comme la transparence référentielle, qui sont utiles pour l'optimisation des compilateurs, le calcul parallèle et distribué et la mise en œuvre de fonctionnalités telles que le versioning, l'undo et le rollback.

Notez également que tout ceci ne traite que des temps d'exécution asymptotiques. De nombreuses techniques d'implémentation de structures de données purement fonctionnelles entraînent un certain ralentissement en facteur constant, dû à la comptabilité supplémentaire nécessaire à leur fonctionnement et aux détails d'implémentation du langage en question. Les avantages des structures de données purement fonctionnelles peuvent l'emporter sur ces ralentissements à facteur constant. Vous devrez donc généralement faire des compromis en fonction du problème en question.

Références

52 votes

Pippinger est l'autorité incontestée sur cette question. Mais nous devons souligner que ses résultats sont théorique pas pratique. Lorsqu'il s'agit de rendre les structures de données fonctionnelles pratiques et efficaces, vous ne pouvez pas faire mieux que Okasaki.

1 votes

Wow, merci, Brian, c'est très intéressant à savoir ! Puisque j'ai eu du mal avec l'explication de Pippenger, puis-je te demander : ces algorithmes "ne peuvent pas faire mieux" sont-ils des cas pathologiques construits artificiellement pour montrer où il y a une différence infranchissable entre les systèmes purs et impurs, ou sont-ils représentatifs des algorithmes que l'on rencontre régulièrement "dans la nature" ? Merci !

7 votes

Itowlson : Je dois admettre que je n'ai pas lu suffisamment de Pippenger pour répondre à votre question ; il a été publié dans une revue à comité de lecture, cité par Okasaki, et j'en ai lu suffisamment pour déterminer que ses affirmations sont pertinentes pour cette question, mais pas assez pour comprendre la preuve. La conclusion immédiate que j'en tire pour les conséquences dans le monde réel est qu'il est trivial de convertir un système O( n ) en un algorithme impur O( n journal n ) pure, en simulant simplement la mémoire modifiable à l'aide d'un arbre binaire équilibré. Il existe des problèmes qui ne peuvent pas faire mieux que cela ; je ne sais pas s'ils sont purement théoriques.

44voto

jkff Points 2939

Il existe en effet plusieurs algorithmes et structures de données pour lesquels aucune solution purement fonctionnelle asymptotiquement efficace (c'est-à-dire implémentable en lambda-calcul pur) n'est connue, même avec la paresse.

  • L'union-trouvée susmentionnée
  • Tables de hachage
  • Tableaux
  • Quelques algorithmes de graphes
  • ...

Cependant, nous supposons que dans les langages "impératifs", l'accès à la mémoire est O(1) alors qu'en théorie, ce n'est pas le cas asymptotiquement (c'est-à-dire pour des problèmes de taille non limitée) et l'accès à la mémoire dans un énorme ensemble de données est toujours O(log n), ce qui peut être émulé dans un langage fonctionnel.

Il faut également se rappeler que tous les langages fonctionnels modernes fournissent des données mutables, et que Haskell le fait même sans sacrifier la pureté (la monade ST).

3 votes

Si l'ensemble de données tient dans la mémoire physique, l'accès à celui-ci est O(1) en ce sens qu'il est possible de trouver une limite supérieure absolue sur le temps nécessaire à la lecture de tout élément. Si l'ensemble de données ne tient pas dans la mémoire physique, on parle d'E/S et ce sera de loin le facteur dominant, quelle que soit la façon dont le programme est écrit.

1 votes

Bien sûr, je parle de O(log n) opérations d'accès à la mémoire externe. Mais de toute façon, je parlais de bs : la mémoire externe peut aussi être O(1)-adressable...

3 votes

Je pense que l'un des principaux avantages de la programmation impérative par rapport à la programmation fonctionnelle est la possibilité de conserver des références à de nombreux aspects distincts d'un état, et de générer un nouvel état tel que toutes ces références pointent vers les aspects correspondants du nouvel état. L'utilisation de la programmation fonctionnelle exigerait que les opérations de déréférencement direct soient remplacées par des opérations de consultation pour trouver l'aspect approprié d'une version particulière de l'état global actuel.

36voto

Pascal Cuoq Points 39606

Cet article prétend que les implémentations connues purement fonctionnelles de l'algorithme de recherche d'union ont tous une complexité asymptotique pire que celui qu'ils publient, qui a une interface purement fonctionnelle mais utilise des données mutables en interne.

Le fait que d'autres réponses affirment qu'il ne peut jamais y avoir de différence et que, par exemple, le seul "inconvénient" du code purement fonctionnel est qu'il peut être parallélisé vous donne une idée de l'information/objectivité de la communauté de la programmation fonctionnelle sur ces questions.

EDIT :

Les commentaires ci-dessous soulignent qu'une discussion biaisée sur les avantages et les inconvénients de la programmation fonctionnelle pure peut ne pas provenir de la "communauté de la programmation fonctionnelle". C'est un bon point. Peut-être que les défenseurs que je vois sont simplement, pour citer un commentaire, "illettrés".

Par exemple, je pense que cette article de blog est écrit par quelqu'un dont on peut dire qu'il est représentatif de la communauté de la programmation fonctionnelle, et puisqu'il s'agit d'une liste de "points pour l'évaluation paresseuse", ce serait un bon endroit pour mentionner tout inconvénient que la programmation paresseuse et purement fonctionnelle pourrait avoir. Un bon endroit aurait été à la place du rejet suivant (techniquement vrai, mais biaisé au point de ne pas être drôle) :

Si une fonction stricte a une complexité O(f(n)) dans un langage strict, alors elle a aussi une complexité O(f(n)) dans un langage paresseux. Pourquoi s'inquiéter ? :)

4voto

Brian Points 1648

Avec une limite supérieure fixe sur l'utilisation de la mémoire, il ne devrait pas y avoir de différence.

Esquisse de la preuve : Étant donné une limite supérieure fixe sur l'utilisation de la mémoire, on devrait pouvoir écrire une machine virtuelle qui exécute un jeu d'instructions impératives avec la même complexité asymptotique que si vous exécutiez réellement sur cette machine. Il en est ainsi parce que vous pouvez gérer la mémoire mutable comme une structure de données persistante, ce qui donne O(log(n)) de lecture et d'écriture, mais avec une limite supérieure fixe sur l'utilisation de la mémoire, vous pouvez avoir une quantité fixe de mémoire, ce qui entraîne une décroissance à O(1). Ainsi, l'implémentation fonctionnelle peut être la version impérative s'exécutant dans l'implémentation fonctionnelle de la VM, et donc ils devraient tous deux avoir la même complexité asymptotique.

7 votes

Une limite supérieure fixe sur l'utilisation de la mémoire n'est pas la façon dont les gens analysent ce genre de choses ; vous supposez une mémoire arbitrairement grande, mais finie. Lors de l'implémentation d'un algorithme, je m'intéresse à la manière dont il s'adaptera à l'entrée la plus simple jusqu'à une taille d'entrée arbitraire. Si l'on fixe une limite supérieure à l'utilisation de la mémoire, pourquoi ne pas également fixer une limite supérieure au temps que le calcul peut prendre, et dire que tout est O(1) ?

0 votes

@Brian Campbell : C'est vrai. Je suggère simplement que si vous le vouliez, vous pourriez ignorer la différence du facteur constant dans de nombreux cas en pratique. Il faudrait quand même être attentif à la différence lors d'un compromis entre la mémoire et le temps pour s'assurer que l'utilisation de m fois plus de mémoire diminue votre temps d'exécution d'au moins un facteur de log(m).

1voto

Kornel Kisielewicz Points 26556

Je vous suggère de lire sur les performances de Haskell puis de jeter un coup d'œil à Alioth Language Shootout les performances des langages fonctionnels par rapport aux langages procéduraux/OO.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X