I understand that there are places where lazy evaluation prevents calculations from being needed. For example, adding two numbers then passing them into a function argument that ends up never being used.
But it seems to me like there would be a lot of overhead to storing and loading the operations that are to be used later and that more often than not that overhead would cancel out any gains.
Can someone address this?
To implement lazy evaluation in our interpreter we need to modify the applica- tion expression evaluation rule to delay evaluating the operand expressions until they are needed. To do this, we introduce a new datatype known as a thunk. We define a Python class, Thunk for representing thunks.
In programming language theory, lazy evaluation, or call-by-need, is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations (sharing).
Lazy Evaluation − AdvantagesIt reduces the time complexity of an algorithm by discarding the temporary computations and conditionals. It allows the programmer to access components of data structures out-of-order after initializing them, as long as they are free from any circular dependencies.
Lazy evaluation is a form of non-strict evaluation in which arguments are not evaluated until required. A computation that does not terminate under strict evaluation, can terminate under lazy evaluation.
Lazy evaluation is an evaluation strategy which holds the evaluation of an expression until its value is needed. It avoids repeated evaluation. Haskell is a good example of such a functional programming language whose fundamentals are based on Lazy Evaluation.
In the absence of side effects, lazy evaluation has the same semantics as normal-order evaluation, but the implementation keeps track of which expressions have already been evaluated, so it can reuse their values if they are needed more than once in a given referencing environment. A delay ed expression is sometimes called a promise.
Scala | Lazy Evaluation. Lazy evaluation or call-by-need is a evaluation strategy where an expression isn’t evaluated until its first use i.e to postpone the evaluation till its demanded. Functional programming languages like Haskell use this strategy extensively.
Just like lazy students, a lazy compiler can be much more efficient if it makes smart choices. For lazy evaluation to be efficient, it needs to use memoization. In other words, it needs to keep a dictionary where the key is the name of the variable, and the value is the result of the evaluation.
You're right. Lazy evaluation does have significant overhead, and most of the time you won't get practical performance gains from it. The main reason for lazy evaluation is that it is convenient -- it makes Haskell's language semantics cleaner, and (for example) lazy/infinite lists can sometimes be handy for the programmer.
Fortunately, the compiler can often optimize lazy mechanics out of the inner loop, where a naive implementation would otherwise incur a substantial performance penalty.
Unexpectedly, it turns out that GHC's implementation of lazy evaluation doesn't introduce appreciable overhead over similar strict runtime systems. The apparent "overhead" of creating a thunk for lazy evaluation (i.e., "storing" the operation to be used later) and eventually forcing its evaluation (i.e., "loading" it) replaces the strict evaluation "overhead" of creating a stack frame, calling the function, and returning. As a result, the cost of a function call is shifted in time but not significantly increased.
It is true that strictness (either explicitly introduced by the programmer or automatically identified by the compiler) is sometimes necessary for good performance, but that's usually because strictness allows unboxing and related optimizations or, in some cases, avoids costly memory leaks which lead to excessive garbage collection overhead. Lazy evaluation itself is not significantly more costly than strict evaluation.
In this answer I provide a somewhat detailed comparison of function calls in the GHC RTS and a typical Java VM implementation. That answer is focused on memory usage (because the question was about garbage collection), but much of the discussion applies to performance more generally.
Summarizing the relevant bits, if you are trying to determine the overhead in calling a function to multiply two numbers:
bar :: Int -> Int -> Int
bar a b = a * b
as invoked by some other function:
foo :: Int -> Int -> Int -> Int
foo x y z = let u = bar y z in x + u
then in a typical strict implementation, like the Java JVM, the byte code would probably look something like:
public static int bar(int, int);
Code:
stack=2, locals=2, args_size=2
0: iload_0 // push a
1: iload_1 // push b
2: imul // multiply and push result
3: ireturn // pop result and return it
public static int foo(int, int, int);
Code:
stack=2, locals=4, args_size=3
0: iload_1 // push y
1: iload_2 // push z
2: invokestatic bar // call bar, pushing result
5: istore_3 // pop and save to "u"
6: iload_0 // push x
7: iload_3 // push u
8: iadd // add and push result
9: ireturn // pop result and return it
The overhead of the bar
function call (i.e., the difference between the above and if bar
was inlined) looks like it's two argument pushes, the call itself, and the return.
For the lazy version, GHC (without optimization) compiles this code as something like the following pseudocode:
foo [x, y, z] =
u = new THUNK(sat_u) // thunk, 32 bytes on heap
jump: (+) x u
sat_u [] = // saturated closure for "bar y z"
push UPDATE(sat_u) // update frame, 16 bytes on stack
jump: bar y z
bar [a, b] =
jump: (*) a b
The overhead of the lazy bar
function call is the creation of a thunk on the bump heap (as fast as stack) that includes two arguments and a pointer to sat_u
(plus room for the return value, though there's no "cost" for this), and a "call" (not visible in the above code) when the (+)
function forces the value u
by jumping to sat_u
. The update frame more or less replaces the return. (In this case, it can be optimized away.)
The bottom line is that, at least to a first approximation, lazy evaluation as implemented in GHC is roughly as fast as strict evaluation, even when everything is actually evaluated.
It works because compiler optimization try to remove laziness when it isn't necessary.
if it sees that next computation will consume results of previous computation it just generate code strict code.
Since Haskell is pure language, lazy evalutions plays an important role. This is not just a feature of the language that allows the programmer to write in a diclarative style without worrying about the sequence of calculations.
Let's take an example.
`sum $ map (^2) [0..100]`
What will happen here in strict semantics. The map
function is evaluated first. It will use the entire input list and generate (with allocation because Haskell is pure) output list. Then sum
will calculate the result.
But in lazy semantics, the intermediate list in this example will not be constructed. So there will be no unnecessary work with memory. Means less work for the garbage collector.
Therefore, for pure languages, lazy semantics is one way to avoid overheads of constructing intermediate objects.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With