Explain demand-driven mechanism in detail.
Demand-Driven Mechanism (Lazy Execution) in Advanced Computer Architecture
The demand-driven mechanism, also called lazy execution, is a computation strategy where work is performed only when its result is actually needed. Instead of executing operations as soon as their inputs become available (data-driven), demand-driven systems start from the final result and pull in the required computations on demand. This approach is common in functional languages and in certain architectural models designed to reduce unnecessary work and synchronize naturally.
Key Idea
- Trigger by need: A computation starts when a consumer requests a value (e.g., the program needs the final output), not when operands arrive.
- Deferred computations (thunks): Expressions are stored as placeholders that describe how to compute a value. They are evaluated only if and when demanded.
- Sharing and memoization: Once a demanded value is computed, the result is stored and reused for subsequent demands, avoiding duplicate work.
- Natural synchronization: Dependencies are respected automatically because a value cannot be consumed until it is produced in response to a demand.
How Demand-Driven Execution Works (Step-by-Step)
- Create thunks: When the program builds an expression, each subexpression is kept as a thunk (a suspended computation).
- Demand propagates backward: When the final result is requested, the system asks for the values it depends on, recursively demanding only what is necessary.
- Evaluate on first use: The first time a thunk is demanded, it is computed and the thunk is updated with its value (memoized).
- Reuse results: Any future demand for the same value reads the memoized result directly, avoiding recomputation.
- Stop early when possible: If a control path makes some computations unnecessary (e.g., short-circuit evaluation), those thunks are never forced.
Illustrative Example
// Expression with control: result = if condition(x) then f(a) else g(b) // Demand-driven flow: 1) 'result' is demanded by the caller. 2) To compute result, we first demand 'condition(x)'. 3) Suppose condition(x) is true: - Demand-driven system evaluates only f(a). - g(b) remains a thunk and is never computed. 4) 'result' becomes the value of f(a).
In contrast, a data-driven approach might prepare to compute both f(a) and g(b) as soon as their inputs are ready, potentially doing unnecessary work.
Demand-Driven vs Data-Driven (At a Glance)
- Evaluation order: Demand-driven starts from the final result and pulls needed computations; data-driven pushes computations when operands arrive.
- Work efficiency: Demand-driven can skip unused branches, improving efficiency in control-heavy code.
- Parallelism: Data-driven usually exposes more inherent parallelism; demand-driven may limit parallel work to what is required by current demands.
- Overheads: Demand-driven adds overhead for thunks, indirections, and updates; data-driven has overhead for fine-grained synchronization and token management.
Architectural and Runtime Support
- Graph reduction: Programs are represented as graphs; nodes are reduced (evaluated) only when their values are demanded. After evaluation, nodes are updated to store results.
- Thunks and indirection nodes: A thunk points to code plus operands; after evaluation, it is replaced with the computed value for fast reuse.
- Work-stealing schedulers: In parallel runtimes, worker threads pull ready demands; when a thunk is forced, dependent tasks may spawn or steal sub-demands.
- Granularity control: Systems often batch small thunks to reduce overhead and improve cache locality.
Benefits
- Avoids unnecessary computation: Only the demanded path is executed (useful with conditionals and short-circuit operations).
- Improved responsiveness: Can prioritize critical results first since demands define the execution frontier.
- Supports infinite or large structures: Values like streams are generated as needed.
- Implicit synchronization: Dependencies are enforced naturally when demanding values.
Limitations
- Overhead of laziness: Creating and managing thunks and indirections can add time and memory costs.
- Reduced parallel exposure: By computing only demanded paths, some available parallelism may remain untapped.
- Space leaks: Deferred computations may accumulate if not carefully managed, increasing memory pressure.
- Performance predictability: Execution order depends on demand patterns, which can be harder to analyze.
Where It’s Used
- Lazy functional languages: Many compilers and runtimes implement demand-driven evaluation with graph reduction and memoization.
- Short-circuit and conditional evaluation: Hardware and compilers commonly avoid computing unused branches.
- Speculative or on-demand services: Systems may fetch, compute, or precompute only upon request to save resources.
Simple Walkthrough Example
// Expression: E = (p() && q()) ? r() : s() Demand-driven: 1) Demand E. 2) Demand p(). If p() returns false, 'q()' is never demanded. 3) If p() is false, demand s(); r() is never computed. 4) If p() is true, demand q(). 5) If q() is true, demand r(); else demand s(). Only the necessary functions are executed, and any demanded result is memoized for reuse.
Summary
The demand-driven mechanism evaluates computations only when their results are required. By using thunks, memoization, and graph reduction, it reduces unnecessary work and synchronizes naturally, making it well-suited for control-intensive programs and large or infinite data structures. However, it introduces overheads and may expose less parallelism than data-driven models. Understanding when to use demand-driven execution helps in designing high-performance systems and choosing appropriate runtime strategies in advanced computer architecture.
