Pourquoi l'évaluation paresseuse n'est-elle pas utilisée partout?


32

Je viens d'apprendre le fonctionnement de l'évaluation paresseuse et je me demandais: pourquoi l'évaluation paresseuse n'est-elle pas appliquée à tous les logiciels actuellement produits? Pourquoi toujours utiliser une évaluation enthousiaste?


2
Voici un exemple de ce qui peut arriver si vous mélangez l'état mutable et l'évaluation paresseuse. alicebobandmallory.com/articles/2011/01/01/…
Jonas Elfström

2
@ JonasElfström: S'il vous plaît, ne confondez pas l'état mutable avec l'une de ses implémentations possibles. L'état mutable peut être implémenté à l'aide d'un flux de valeurs infini et paresseux. Ensuite, vous n'avez pas le problème des variables mutables.
Giorgio

Dans les langages de programmation impératifs, «l'évaluation paresseuse» nécessite un effort conscient de la part du programmeur. La programmation générique dans les langages impératifs a rendu cela facile, mais elle ne sera jamais transparente. La réponse à l'autre côté de la question fait ressortir une autre question: "Pourquoi les langages de programmation fonctionnels ne sont-ils pas utilisés partout?", Et la réponse actuelle est simplement "non" dans le cadre de l'actualité.
rwong

2
Les langages de programmation fonctionnels ne sont pas utilisés partout pour la même raison que nous n'utilisons pas de marteaux sur les vis, tous les problèmes ne peuvent pas être facilement exprimés en entrée fonctionnelle -> en sortie, l'interface graphique par exemple est mieux adaptée pour être exprimée de manière impérative .
ALXGTV

En outre, il existe deux classes de langages de programmation fonctionnels (ou du moins les deux prétendent être fonctionnels), les langages fonctionnels impératifs, par exemple Clojure, Scala et le déclaratif, par exemple Haskell, OCaml.
ALXGTV

Réponses:


38

L'évaluation paresseuse nécessite des frais généraux de tenue de livres - vous devez savoir si elle a déjà été évaluée et de telles choses. L'évaluation désireuse est toujours évaluée, vous n'avez donc pas besoin de le savoir. Cela est particulièrement vrai dans des contextes concurrents.

Deuxièmement, il est trivial de convertir une évaluation enthousiaste en une évaluation paresseuse en l'empaquetant en un objet fonction à appeler plus tard, si vous le souhaitez.

Troisièmement, une évaluation paresseuse implique une perte de contrôle. Que faire si j'évalue paresseusement la lecture d'un fichier à partir d'un disque? Ou prendre le temps? Ce n'est pas acceptable.

Une évaluation désirée peut être plus efficace et plus contrôlable, et est trivialement convertie en évaluation paresseuse. Pourquoi voudriez-vous une évaluation paresseuse?


10
La lecture paresseuse d'un fichier à partir du disque est en fait très nette - pour la plupart de mes programmes et scripts simples, Haskell readFileest exactement ce dont j'ai besoin. De plus, la conversion d'une évaluation paresseuse à une évaluation avide est tout aussi triviale.
Tikhon Jelvis

3
D'accord avec vous tous sauf le dernier paragraphe. L'évaluation paresseuse est plus efficace lorsqu'il y a une opération en chaîne, et elle peut avoir plus de contrôle sur le moment où vous avez réellement besoin des données
texasbruce

4
Les lois des foncteurs aimeraient vous parler de la "perte de contrôle". Si vous écrivez des fonctions pures qui fonctionnent sur des types de données immuables, l'évaluation paresseuse est une aubaine. Des langues comme haskell sont fondamentalement basées sur le concept de paresse. C'est lourd dans certaines langues, en particulier lorsqu'il est mélangé avec du code "dangereux", mais vous donnez l'impression que la paresse est dangereuse ou mauvaise par défaut. C'est seulement "dangereux" dans un code dangereux.
sara

1
@DeadMG Pas si vous vous souciez de savoir si votre code se termine ou non ... Qu'est-ce que cela head [1 ..]vous donne dans un langage pur évalué avec impatience, car dans Haskell, cela donne 1?
point

1
Pour de nombreuses langues, la mise en œuvre d'une évaluation paresseuse introduira à tout le moins une complexité. Parfois, cette «complexité est nécessaire, et avoir l'évaluation paresseuse améliore l'efficacité globale - en particulier si ce qui est évalué n'est que conditionnellement nécessaire. Cependant, mal fait, il peut introduire des bogues subtils ou des problèmes de performances difficiles à expliquer en raison de mauvaises hypothèses lors de l'écriture du code. Il y a un compromis.
Berin Loritsch

17

Principalement parce que le code et l'état paresseux peuvent mal se mélanger et provoquer des bogues difficiles à trouver. Si l'état d'un objet dépendant change, la valeur de votre objet paresseux peut être incorrecte lors de l'évaluation. Il est préférable que le programmeur code explicitement l'objet pour qu'il soit paresseux lorsqu'il sait que la situation est appropriée.

D'un côté, Haskell utilise l'évaluation paresseuse pour tout. Ceci est possible car c'est un langage fonctionnel et n'utilise pas d'état (sauf dans quelques circonstances exceptionnelles où ils sont clairement marqués)


Oui, état mutable + évaluation paresseuse = mort. Je pense que les seuls points que j'ai perdus lors de ma finale SICP concernaient l'utilisation set!dans un interprète Scheme paresseux. > :(
Tikhon Jelvis

3
"le code paresseux et l'état peuvent mal se mélanger": cela dépend vraiment de la façon dont vous implémentez l'état. Si vous l'implémentez à l'aide de variables mutables partagées et que vous dépendez de l'ordre d'évaluation pour que votre état soit cohérent, alors vous avez raison.
Giorgio

14

L'évaluation paresseuse n'est pas toujours meilleure.

Les avantages de l'évaluation paresseuse en termes de performances peuvent être excellents, mais il n'est pas difficile d'éviter la plupart des évaluations inutiles dans des environnements enthousiastes - certainement paresseux la rend facile et complète, mais rarement l'évaluation inutile dans le code est un problème majeur.

La bonne chose à propos de l'évaluation paresseuse est qu'elle vous permet d'écrire du code plus clair; obtenir le 10e nombre premier en filtrant une liste de nombres naturels infinis et en prenant le 10e élément de cette liste est l'une des façons les plus concises et claires de procéder: (pseudocode)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

Je pense qu'il serait assez difficile d'exprimer les choses avec autant de concision sans paresse.

Mais la paresse n'est pas la réponse à tout. Pour commencer, la paresse ne peut pas être appliquée de manière transparente en présence de l'état, et je pense que l'état ne peut pas être détecté automatiquement (sauf si vous travaillez par exemple dans Haskell, lorsque l'état est assez explicite). Ainsi, dans la plupart des langues, la paresse doit être effectuée manuellement, ce qui rend les choses moins claires et supprime ainsi l'un des grands avantages de l'évaluation paresseuse.

En outre, la paresse présente des inconvénients en termes de performances, car elle entraîne une surcharge importante de conservation des expressions non évaluées; ils utilisent du stockage et ils sont plus lents à travailler qu'avec des valeurs simples. Il n'est pas rare de découvrir que vous devez coder avec impatience car la version paresseuse est lente et il est parfois difficile de raisonner sur les performances.

Comme cela a tendance à se produire, il n'y a pas de meilleure stratégie absolue. Lazy est génial si vous pouvez écrire un meilleur code en tirant parti de structures de données infinies ou d'autres stratégies qu'il vous permet d'utiliser, mais il peut être plus facile d'optimiser.


Serait-il possible pour un compilateur vraiment intelligent d'atténuer considérablement la surcharge. ou même profiter de la paresse pour des optimisations supplémentaires?
Tikhon Jelvis

3

Voici une brève comparaison des avantages et des inconvénients d'une évaluation avide et paresseuse:

  • Évaluation désireuse:

    • Frais généraux potentiels liés à l'évaluation inutile de choses.

    • Évaluation rapide et sans entrave.

  • Évaluation paresseuse:

    • Aucune évaluation inutile.

    • Frais généraux de comptabilité à chaque utilisation d'une valeur.

Donc, si vous avez de nombreuses expressions qui ne doivent jamais être évaluées, paresseux est préférable; pourtant, si vous n'avez jamais une expression qui n'a pas besoin d'être évaluée, paresseux est une surcharge pure.

Maintenant, jetons un œil aux logiciels du monde réel: combien de fonctions que vous écrivez ne nécessitent pas l' évaluation de tous leurs arguments? Surtout avec les fonctions courtes modernes qui ne font qu'une chose, le pourcentage de fonctions entrant dans cette catégorie est très faible. Ainsi, une évaluation paresseuse introduirait la plupart du temps les frais de tenue de livres, sans avoir la possibilité d'enregistrer quoi que ce soit.

Par conséquent, une évaluation paresseuse ne paie tout simplement pas en moyenne, une évaluation désireuse est la meilleure solution pour le code moderne.


1
"Frais généraux de tenue de livres à chaque utilisation d'une valeur.": Je ne pense pas que les frais généraux de tenue de livres soient plus importants que, par exemple, la recherche de références nulles dans un langage comme Java. Dans les deux cas, vous devez vérifier un bit d'information (évalué / en attente par rapport à nul / non nul) et vous devez le faire chaque fois que vous utilisez une valeur. Donc, oui, il y a des frais généraux, mais c'est minime.
Giorgio

1
"Combien de fonctions que vous écrivez ne nécessitent pas l'évaluation de tous leurs arguments?": Ceci n'est qu'un exemple d'application. Qu'en est-il des structures de données récursives et infinies? Pouvez-vous les mettre en œuvre avec une évaluation enthousiaste? Vous pouvez utiliser des itérateurs, mais la solution n'est pas toujours aussi concise. Bien sûr, vous ne manquez probablement pas quelque chose que vous n'avez jamais eu l'occasion d'utiliser intensivement.
Giorgio

2
"Par conséquent, une évaluation paresseuse ne paie tout simplement pas en moyenne, une évaluation enthousiaste est la meilleure solution pour le code moderne.": Cette affirmation ne tient pas: cela dépend vraiment de ce que vous essayez d'implémenter.
Giorgio

1
@Giorgio Le surcoût peut ne pas vous sembler grand-chose, mais les conditions sont l'une des choses auxquelles les processeurs modernes aspirent: une branche mal prévue force généralement une vidange complète du pipeline, jetant le travail de plus de dix cycles de CPU. Vous ne voulez pas de conditions inutiles dans votre boucle intérieure. Payer dix cycles supplémentaires par argument de fonction est presque aussi inacceptable pour un code sensible aux performances que de coder la chose en java. Vous avez raison de dire que l'évaluation paresseuse vous permet de réaliser des astuces que vous ne pouvez pas facilement faire avec une évaluation avide. Mais la grande majorité du code n'a pas besoin de ces astuces.
cmaster

2
Cela semble être une réponse par inexpérience des langues avec évaluation paresseuse. Par exemple, qu'en est-il des structures de données infinies?
Andres F.

3

Comme l'a noté @DeadMG, l'évaluation paresseuse nécessite des frais de tenue de livres. Cela peut être coûteux par rapport à une évaluation avide. Considérez cette déclaration:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

Cela prendra un peu de calcul pour calculer. Si j'utilise une évaluation paresseuse, je dois vérifier si elle a été évaluée à chaque fois que je l'utilise. Si cela se trouve dans une boucle étroite fortement utilisée, les frais généraux augmentent considérablement, mais il n'y a aucun avantage.

Avec une évaluation enthousiaste et un compilateur décent, la formule est calculée au moment de la compilation. La plupart des optimiseurs déplacent l'affectation de toutes les boucles dans lesquelles elle se produit, le cas échéant.

L'évaluation paresseuse est la mieux adaptée au chargement de données qui seront rarement consultées et a une surcharge élevée à récupérer. Il est donc plus approprié de border les cas que la fonctionnalité de base.

En général, il est recommandé d'évaluer les éléments fréquemment consultés le plus tôt possible. L'évaluation paresseuse ne fonctionne pas avec cette pratique. Si vous aurez toujours accès à quelque chose, tout ce que l'évaluation paresseuse fera sera d'ajouter des frais généraux. Le coût / avantage de l'utilisation de l'évaluation paresseuse diminue à mesure que l'élément auquel on accède devient moins susceptible d'être consulté.

Toujours utiliser l'évaluation paresseuse implique également une optimisation précoce. Il s'agit d'une mauvaise pratique qui se traduit souvent par un code beaucoup plus complexe et coûteux qui pourrait autrement être le cas. Malheureusement, une optimisation prématurée se traduit souvent par un code plus lent que le code simple. Jusqu'à ce que vous puissiez mesurer l'effet de l'optimisation, c'est une mauvaise idée d'optimiser votre code.

Éviter une optimisation prématurée n'entre pas en conflit avec les bonnes pratiques de codage. Si les bonnes pratiques n'ont pas été appliquées, les optimisations initiales peuvent consister à appliquer de bonnes pratiques de codage telles que le déplacement des calculs hors des boucles.


1
Vous semblez argumenter par inexpérience. Je vous suggère de lire l'article "Pourquoi la programmation fonctionnelle est importante" par Wadler. Il consacre une section importante expliquant le pourquoi de l'évaluation paresseuse (indice: cela n'a pas grand-chose à voir avec les performances, l'optimisation précoce ou le "chargement de données rarement consultées", et tout ce qui concerne la modularité).
Andres F.

@AndresF J'ai lu le document auquel vous vous référez. Je suis d'accord avec l'utilisation de l'évaluation paresseuse dans de tels cas. Une évaluation précoce peut ne pas être appropriée, mais je dirais que le retour du sous-arbre pour le mouvement sélectionné peut avoir un avantage significatif si des mouvements supplémentaires peuvent être ajoutés facilement. Cependant, la création de cette fonctionnalité pourrait être une optimisation prématurée. En dehors de la programmation fonctionnelle, j'ai des problèmes importants avec l'utilisation de l'évaluation paresseuse et l'échec de l'utilisation de l'évaluation paresseuse. Il existe des rapports sur des coûts de performance importants résultant d'une évaluation paresseuse de la programmation fonctionnelle.
BillThor

2
Tel que? Il existe également des rapports sur des coûts de performance significatifs lors de l'utilisation d'une évaluation enthousiaste (coûts sous la forme d'une évaluation non nécessaire, ainsi que de la non-interruption du programme). Il y a des coûts pour presque toutes les autres fonctionnalités (mal) utilisées, pensez-y. La modularité elle-même peut avoir un coût; la question est de savoir si cela en vaut la peine.
Andres F.

3

Si nous devons potentiellement évaluer complètement une expression pour déterminer sa valeur, une évaluation paresseuse peut être un inconvénient. Disons que nous avons une longue liste de valeurs booléennes et que nous voulons savoir si toutes sont vraies:

[True, True, True, ... False]

Pour ce faire, nous devons examiner tous les éléments de la liste, quoi qu'il arrive, il n'y a donc aucune possibilité de suspendre paresseusement l'évaluation. Nous pouvons utiliser un pli pour déterminer si toutes les valeurs booléennes de la liste sont vraies. Si nous utilisons un repli vers la droite, qui utilise l'évaluation paresseuse, nous n'obtenons aucun des avantages de l'évaluation paresseuse, car nous devons examiner chaque élément de la liste:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

Un pli à droite sera beaucoup plus lent dans ce cas qu'un pli strict à gauche, qui n'utilise pas d'évaluation paresseuse:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

La raison en est qu'un repli strict à gauche utilise la récursivité de la queue, ce qui signifie qu'il accumule la valeur de retour et ne crée pas et ne stocke pas en mémoire une grande chaîne d'opérations. C'est beaucoup plus rapide que le pli paresseux à droite car les deux fonctions doivent de toute façon regarder la liste entière et le pli à droite ne peut pas utiliser la récursivité de la queue. Donc, le fait est que vous devez utiliser ce qui est le mieux pour la tâche à accomplir.


"Donc, le fait est que vous devez utiliser ce qui est le mieux pour la tâche à accomplir." +1
Giorgio
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.