Ich habe gerade gelernt, wie eine verzögerte Evaluierung funktioniert, und habe mich gefragt, warum die verzögerte Evaluierung nicht in jeder derzeit produzierten Software angewendet wird. Warum noch eifrige Auswertung verwenden?
Ich habe gerade gelernt, wie eine verzögerte Evaluierung funktioniert, und habe mich gefragt, warum die verzögerte Evaluierung nicht in jeder derzeit produzierten Software angewendet wird. Warum noch eifrige Auswertung verwenden?
Antworten:
Eine verzögerte Bewertung erfordert einen Aufwand für die Buchhaltung. Sie müssen wissen, ob sie bereits bewertet wurde, und solche Dinge. Die eifrige Bewertung wird immer bewertet, sodass Sie es nicht wissen müssen. Dies gilt insbesondere in gleichzeitigen Kontexten.
Zweitens ist es trivial , eifrige Auswertungen in verzögerte Auswertungen umzuwandeln, indem Sie sie in ein Funktionsobjekt packen, das später aufgerufen wird, wenn Sie dies wünschen.
Drittens impliziert eine faule Bewertung einen Kontrollverlust. Was ist, wenn ich das Lesen einer Datei von einer Festplatte faul auswerte? Oder die Zeit bekommen? Das ist nicht akzeptabel.
Die eifrige Bewertung kann effizienter und kontrollierbarer sein und wird trivial in eine verzögerte Bewertung umgewandelt. Warum sollten Sie eine faule Bewertung wünschen?
readFile
ist genau das, was ich brauche. Außerdem ist die Umstellung von fauler auf eifrige Bewertung ebenso trivial.
head [1 ..]
gibst du in einer mit Spannung bewerteten reinen Sprache, denn in Haskell gibt es das 1
?
Hauptsächlich, weil fauler Code und Zustand sich schlecht vermischen und einige schwer zu findende Fehler verursachen können. Wenn sich der Status eines abhängigen Objekts ändert, kann der Wert Ihres faulen Objekts bei der Auswertung falsch sein. Es ist viel besser, wenn der Programmierer das Objekt explizit codiert, um faul zu sein, wenn er weiß, dass die Situation angemessen ist.
Nebenbei bemerkt verwendet Haskell Lazy Evaluation für alles. Dies ist möglich, da es sich um eine funktionale Sprache handelt, die keinen Status verwendet (außer in einigen Ausnahmefällen, in denen sie deutlich gekennzeichnet sind).
set!
in einem faulen Schema-Interpreter. > :(
Lazy Evaluation ist nicht immer besser.
Die Leistungsvorteile der verzögerten Evaluierung können groß sein, aber es ist nicht schwer zu vermeiden, dass die meisten unnötigen Evaluierungen in eifrigen Umgebungen durchgeführt werden. Mit Faulheit wird dies einfach und vollständig, aber unnötige Evaluierungen im Code sind selten ein großes Problem.
Das Gute an der verzögerten Auswertung ist, dass Sie damit klareren Code schreiben können. Die zehnte Primzahl durch Filtern einer unendlichen Liste natürlicher Zahlen zu erhalten und das zehnte Element dieser Liste zu nehmen, ist eine der präzisesten und klarsten Vorgehensweisen: (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)
Ich glaube, es wäre ziemlich schwierig, Dinge ohne Faulheit so präzise auszudrücken.
Aber Faulheit ist nicht die Antwort auf alles. Für den Anfang kann Faulheit nicht transparent angewendet werden, wenn der Zustand vorhanden ist, und ich glaube, dass Zustandserhalt nicht automatisch erkannt werden kann (es sei denn, Sie arbeiten beispielsweise in Haskell, wenn der Zustand ziemlich explizit ist). In den meisten Sprachen muss Faulheit also manuell erfolgen, was die Übersichtlichkeit beeinträchtigt und somit einen der großen Vorteile von Lazy Eval zunichte macht.
Darüber hinaus hat Faulheit Leistungsmängel, da sie einen erheblichen Mehraufwand für die Beibehaltung nicht bewerteter Ausdrücke mit sich bringt. Sie belegen Speicherplatz und arbeiten langsamer als einfache Werte. Es ist nicht ungewöhnlich, herauszufinden, dass Sie eifrigen Code benötigen, da die Lazy-Version sehr langsam ist und es manchmal schwierig ist, über die Leistung zu urteilen.
Es gibt keine absolut beste Strategie. Lazy ist großartig, wenn Sie besseren Code schreiben können, indem Sie unendliche Datenstrukturen oder andere Strategien nutzen, die Sie verwenden können, aber eifrig leichter zu optimieren sind.
Hier ein kurzer Vergleich der Vor- und Nachteile eifriger und fauler Bewertungen:
Eifrige Bewertung:
Möglicher Overhead durch unnötiges Bewerten von Inhalten.
Ungehinderte, schnelle Auswertung
Faule Bewertung:
Keine unnötige Auswertung.
Buchhaltungsaufwand bei jeder Verwendung eines Wertes.
Wenn Sie also viele Ausdrücke haben, die niemals ausgewertet werden müssen, ist Faulheit besser. Wenn Sie jedoch nie einen Ausdruck haben, der nicht ausgewertet werden muss, ist Faulheit ein reiner Aufwand.
Lassen Sie uns nun einen Blick auf die Software der realen Welt werfen: Wie viele der Funktionen, die Sie schreiben, müssen nicht alle ihre Argumente ausgewertet werden? Gerade bei den modernen Kurzfunktionen, die nur eines tun, fällt der Anteil der Funktionen in diese Kategorie sehr gering aus. Daher würde eine verzögerte Auswertung den Buchhaltungsaufwand die meiste Zeit nur einleiten, ohne die Möglichkeit zu haben, tatsächlich etwas zu sparen.
Infolgedessen zahlt sich eine verzögerte Evaluierung im Durchschnitt einfach nicht aus, da eine eifrige Evaluierung für modernen Code besser geeignet ist.
Wie @DeadMG feststellte, erfordert eine faule Auswertung einen Buchhaltungsaufwand. Dies kann relativ zu einer eifrigen Bewertung teuer sein. Betrachten Sie diese Aussage:
i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3
Dies erfordert einige Berechnungen. Wenn ich Lazy Evaluation verwende, muss ich bei jeder Verwendung überprüfen, ob es evaluiert wurde. Befindet sich dies in einer stark genutzten engen Schleife, erhöht sich der Overhead erheblich, aber es gibt keinen Vorteil.
Mit eifriger Auswertung und einem anständigen Compiler wird die Formel zur Kompilierzeit berechnet. Die meisten Optimierer verschieben die Zuweisung gegebenenfalls aus den Schleifen, in denen sie auftritt.
Die verzögerte Auswertung eignet sich am besten zum Laden von Daten, auf die nur selten zugegriffen wird, und erfordert einen hohen Abrufaufwand. Es ist daher angemessener, Fälle als die Kernfunktionalität zu kanten.
Im Allgemeinen empfiehlt es sich, häufig verwendete Elemente so früh wie möglich zu bewerten. Lazy Evaluation funktioniert mit dieser Praxis nicht. Wenn Sie immer auf etwas zugreifen, ist eine verzögerte Auswertung lediglich mit einem zusätzlichen Aufwand verbunden. Das Kosten-Nutzen-Verhältnis bei der Verwendung der verzögerten Auswertung sinkt, wenn die Wahrscheinlichkeit, dass auf das Element zugegriffen wird, abnimmt.
Die Verwendung von Lazy Evaluation bedeutet auch eine frühzeitige Optimierung. Dies ist eine schlechte Praxis, die häufig zu Code führt, der viel komplexer und teurer ist, als dies ansonsten der Fall wäre. Leider führt eine vorzeitige Optimierung häufig dazu, dass Code langsamer als einfacher ausgeführt wird. Bis Sie den Effekt der Optimierung messen können, ist es eine schlechte Idee, Ihren Code zu optimieren.
Die Vermeidung vorzeitiger Optimierung steht nicht im Widerspruch zu guten Codierungspraktiken. Wenn keine bewährten Methoden angewendet wurden, können erste Optimierungen darin bestehen, bewährte Codierungsmethoden anzuwenden, z. B. das Verschieben von Berechnungen aus Schleifen.
Wenn wir einen Ausdruck möglicherweise vollständig auswerten müssen, um seinen Wert zu bestimmen, kann eine verzögerte Auswertung ein Nachteil sein. Nehmen wir an, wir haben eine lange Liste von Booleschen Werten und möchten herausfinden, ob sie alle wahr sind:
[True, True, True, ... False]
Um dies zu tun, müssen wir uns jedes Element in der Liste ansehen , egal was passiert, daher gibt es keine Möglichkeit, die Auswertung träge abzubrechen. Wir können eine Falte verwenden, um zu bestimmen, ob alle Booleschen Werte in der Liste wahr sind. Wenn wir ein Fold-Right verwenden, das Lazy Evaluation verwendet, erhalten wir keinen der Vorteile von Lazy Evaluation, da wir uns jedes Element in der Liste ansehen müssen:
foldr (&&) True [True, True, True, ... False]
> 0.27 secs
Eine Falte nach rechts ist in diesem Fall viel langsamer als eine strenge Falte nach links, die keine verzögerte Auswertung verwendet:
foldl' (&&) True [True, True, True, ... False]
> 0.09 secs
Der Grund ist, dass eine strikte Fold-Left-Methode die Schwanzrekursion verwendet, dh, sie akkumuliert den Rückgabewert und baut keine große Kette von Operationen auf und speichert sie im Speicher. Dies ist viel schneller als die Lazy Fold-Funktion, da beide Funktionen ohnehin die gesamte Liste anzeigen müssen und die Fold-Funktion keine Schwanzrekursion verwenden kann. Der Punkt ist also, dass Sie das verwenden sollten, was für die jeweilige Aufgabe am besten ist.