Why Parallel Algorithm Analysis Matters
Adding processors does not automatically divide runtime by their count. Some tasks depend on earlier results, some become ready together, and a processor may sit idle while work elsewhere remains unfinished. Parallel algorithm analysis separates the total computation from the dependency chain that processors cannot shorten.
The four central ideas are work, span, speedup, and Amdahl's law. Work counts all task time. Span measures the longest dependency path. Speedup compares an execution with a stated one-worker or sequential baseline. Amdahl models the limit imposed by a fixed serial fraction when the remaining portion scales ideally.
This lesson represents a computation as a weighted directed acyclic graph, computes its work and span, then simulates identical workers with an explicit deterministic greedy policy. Python, C++, and Java print the same completion batches, ready tasks, assignments, runtime, and exact rational metrics. A separate Amdahl calculation shows a fixed serial/parallel model alongside the DAG experiment.
The purpose is prediction and diagnosis: identify whether more processors could help, whether dependency structure is the bottleneck, and whether a proposed parallelization adds too much work. The same reasoning supports reductions, build pipelines, parallel loops, and task schedulers, provided the graph captures the required dependencies and the duration model is appropriate.
The simulator is sequential software describing an ideal schedule. It creates no threads and measures no hardware execution times. That separation lets you verify the reasoning before adding runtime overhead, communication, contention, or a specific parallel programming system.
Measure total work and the longest weighted dependency path before estimating parallel speed. Any execution needs enough processor-time to do all work and enough elapsed time to traverse the span. A deterministic worker schedule makes these limits visible; Amdahl's law adds a separate fixed-workload model of serial-fraction limits.
Core Concepts and Terminology
A computation DAG is $G=(V,E)$, with $N=\lvert V\rvert$ tasks and $M=\lvert E\rvert$ precedence edges. Task $v$ has positive integer duration $d_v$. An edge $u\to v$ means v may start only after u finishes. Tasks are nonpreemptive: once started, a task runs to completion on one identical worker. The model has no communication delay, scheduling cost, failures, or resource contention.
Work is the sum of durations: $$ W=T_1=\sum_{v\in V}d_v. $$ Here $T_1$ is the time to execute this same DAG on one ideal worker. If the fastest sequential algorithm is different, its runtime is a different baseline; state which one a speedup uses.
Span is the longest weighted directed path: $$ S=T_\infty=\max_{\pi\text{ a directed path}}\sum_{v\in\pi}d_v. $$ With unlimited workers, every task can start as soon as its predecessors finish, so span is the ideal minimum completion time. It includes both source and final task durations. For an empty graph, define $W=S=0$ and completion time zero.
An earliest-start/finish dynamic program computes span in topological order: $$ \operatorname{ES}(v)=\max_{u\to v}\operatorname{EF}(u),\qquad \operatorname{EF}(v)=\operatorname{ES}(v)+d_v. $$ The maximum over no predecessors is zero. Then $S=\max_v\operatorname{EF}(v)$. Store an attaining predecessor to recover one critical path; multiple longest paths may exist. Earliest start assumes unlimited processors and can be earlier than actual start in a limited-worker schedule.
For $P\ge1$ workers and a legal makespan $T_P$, the work and span laws give $$ T_P\ge\max(W/P,S). $$ Integer task times allow the stronger integral lower bound $\max(\lceil W/P\rceil,S)$. Neither bound is generally an achievable runtime: task indivisibility and scheduling constraints can leave a gap.
Average available parallelism is $\mathcal P=W/S$ for a nonempty graph. It is a ratio, not an instantaneous count of ready tasks or a promise that exactly that many workers remain busy. Work-based speedup and efficiency are $$ \operatorname{Speedup}(P)=\frac W{T_P},\qquad \operatorname{Efficiency}(P)=\frac W{PT_P}. $$ Therefore speedup is at most $\min(P,W/S)$ in this model. These definitions and the scheduling bounds are treated in MIT's Races and Parallelism lecture. Empty-graph ratios such as $0/0$ are undefined; the program avoids printing them.
Amdahl's fixed-workload model. Let $\sigma\in[0,1]$ be the fraction of a one-processor execution that stays serial. If the remaining work divides perfectly among P workers with no added overhead, then $$ T_P^{\mathrm{Amdahl}}=T_1\left(\sigma+\frac{1-\sigma}{P}\right),\qquad \operatorname{Speedup}_{\mathrm{Amdahl}}(P)=\frac1{\sigma+(1-\sigma)/P}. $$ For $\sigma\gt0$, the limiting speedup as P grows is $1/\sigma$. For $\sigma=0$, the ideal formula gives P; for $\sigma=1$, it gives one. MIT's Amdahl discussion explains this fixed serial/parallel decomposition. It is a model of a particular workload decomposition, not a universal replacement for a DAG's scheduling analysis.
Do not substitute $S/W$ for $\sigma$ without justifying that decomposition. Span can include a different branch at different joins and includes one task from a parallel region; it is not automatically the sum of globally serial phases. The sample prints a separate $\sigma=1/4$ Amdahl family precisely so its assumptions remain explicit.
| Term | Concrete example | Role |
|---|---|---|
| Task duration | B takes four time units | Weight a node's execution cost |
| Precedence edge | C must finish before E starts | Restrict legal concurrency |
| Work $W$ | Sum of all seven durations is $17$ | Bound total processor-time demand |
| Span $S$ | A–B–E–G totals $10$ | Bound execution even with unlimited workers |
| Ready task | All predecessors completed; task not started | Determine what a free worker may execute |
| Running / completed | E runs on $[6,9)$ and completes at nine | Separate dispatch from dependency release |
| Makespan $T_P$ | Two-worker sample completes at ten | Measure the simulated schedule |
| Parallelism $W/S$ | $17/10=1.7$ | Upper-bound work-based speedup |
| Efficiency | $17/(2\cdot10)=0.85$ | Relate useful work to available worker-time |
| Amdahl serial fraction | $\sigma=1/4$ caps ideal speedup at four | State fixed-workload scaling assumptions |
How Parallel Algorithm Analysis Works, Step by Step
- Validate and canonicalize the DAG. Check positive duration bounds, edge endpoints, no self edges, and no duplicate directed edges. Sort a copy of the edges. Construct successor lists and indegrees without changing the inputs.
- Find a deterministic topological order. Put zero-indegree tasks into a minimum-ID priority queue. Repeatedly pop the smallest ID, update successor earliest-start candidates, and release a successor when its indegree becomes zero. If fewer than N tasks are processed, reject a cycle rather than simulating a deadlock.
- Compute work and span. Sum all durations. For each task in topological order, set finish to earliest start plus duration; propagate that finish to its successors using a maximum. For equal best predecessor finishes, choose the smaller predecessor ID. Choose the smallest-ID maximum-finish endpoint, then follow parent links backward and reverse them to recover one critical path.
- Initialize the worker simulation separately. Restore original indegrees; the topological analysis already consumed a working copy. Ready roots enter a fresh minimum-ID queue. All worker slots begin free. Actual starts/finishes/worker IDs start at $-1$.
- At an event time, finish the entire completion batch. Gather all running tasks finishing now, free their workers, sort completed IDs, and decrement the corresponding successor counts. Release a task only when all its predecessor completions have been processed. All simultaneous completions happen before dispatch at that time.
- Dispatch deterministically and greedily. Scan free workers by increasing worker ID. Assign each the smallest ready task ID and record its start and finish. Continue until all workers are busy or the ready queue is empty. Dispatching a task does not release its successors: completion does.
- Record and advance. Save the completed batch, ready queue before dispatch, started tasks, waiting queue after dispatch, and running tasks in worker order. If every task is completed, return now. Otherwise jump directly to the smallest running finish time and repeat. Positive durations guarantee strictly later completion events.
The program's priority queue is a ready-task policy, not a critical-path priority. Task IDs provide reproducibility but do not imply priority optimality. The scheduler is greedy in the work-conserving sense: it leaves no worker idle while a ready task waits. It can still make a suboptimal choice about which ready task to start.
The step diagram focuses on the sample's join at time nine. E and F complete together. G has two predecessor counts before the batch, one after E's completion, then zero after F's. G is queued once and starts on worker zero; worker one remains idle because no other task is ready. The final completion at time ten is recorded as a separate event.
flowchart TD
A["Validate durations and edges; build graph"] --> B["Topological work/span analysis"]
B --> C{"Cycle detected?"}
C -->|Yes| D["Reject graph"]
C -->|No| E["Initialize ready tasks and free workers"]
E --> F{"Graph empty?"}
F -->|Yes| L["Return makespan zero and empty events"]
F -->|No| G["Complete all tasks finishing now; release successors"]
G --> H["Assign smallest ready IDs to free workers in order"]
H --> I["Record completion, ready, start and running state"]
I --> J{"All tasks completed?"}
J -->|Yes| K["Return schedule and metrics"]
J -->|No| M["Jump to earliest running finish time"]
M --> G
Worked Example with a Full Trace
Use task IDs zero through six, called A through G in the diagrams. Durations and all predecessor sets are:
| Task / ID | Duration | Predecessors |
|---|---|---|
| A / 0 | 2 | None |
| B / 1 | 4 | A |
| C / 2 | 3 | A |
| D / 3 | 2 | A |
| E / 4 | 3 | B, C |
| F / 5 | 2 | C, D |
| G / 6 | 1 | E, F |
Full unlimited-worker analysis. Kahn's minimum-ID order is $[0,1,2,3,4,5,6]$. The table gives every earliest-start/finish state and the chosen critical predecessor. Work accumulates by adding each row's duration. The parent selection uses earliest finish, not actual limited-worker finish.
| Task | ES calculation | Earliest finish | Chosen parent | Work so far |
|---|---|---|---|---|
| A | 0 | 2 | — | 2 |
| B | 2 | 6 | A | 6 |
| C | 2 | 5 | A | 9 |
| D | 2 | 4 | A | 11 |
| E | max(6,5)=6 | 9 | B | 14 |
| F | max(5,4)=5 | 7 | C | 16 |
| G | max(9,7)=9 | 10 | E | 17 |
Thus $W=17$, $S=10$, and the recovered path is $[0,1,4,6]$, or A–B–E–G. F could finish at time seven with unlimited workers, but that does not force the two-worker execution to finish F at seven.
Every event with two workers. Assign the smallest ready ID to the smallest free worker. Ready means ready immediately before dispatch; waiting means ready after dispatch. A task shown as running has been started but is still an unfinished predecessor. Worker labels are zero and one.
| Time | Completed | Ready before | Started: task / worker / finish | Waiting after | Running after dispatch |
|---|---|---|---|---|---|
| 0 | [] | [A] | [A / 0 / 2] | [] | [A / 0 / 2] |
| 2 | [A] | [B,C,D] | [B / 0 / 6, C / 1 / 5] | [D] | [B / 0 / 6, C / 1 / 5] |
| 5 | [C] | [D] | [D / 1 / 7] | [] | [B / 0 / 6, D / 1 / 7] |
| 6 | [B] | [E] | [E / 0 / 9] | [] | [E / 0 / 9, D / 1 / 7] |
| 7 | [D] | [F] | [F / 1 / 9] | [] | [E / 0 / 9, F / 1 / 9] |
| 9 | [E,F] | [G] | [G / 0 / 10] | [] | [G / 0 / 10] |
| 10 | [G] | [] | [] | [] | [] |
At time two, B, C, D all become ready, but only two workers are available. B and C start, leaving D waiting. C finishes at five, so D takes worker one from five to seven. B finishes at six and releases E because C has already completed. E runs on worker zero from six to nine. At seven, D's completion releases F, which runs on worker one until nine. G waits for both E and F, then runs from nine to ten.
All actual assignments, alongside their unlimited-worker earliest starts, are:
| Task | Earliest start | Actual start | Actual finish | Worker |
|---|---|---|---|---|
| A | 0 | 0 | 2 | 0 |
| B | 2 | 2 | 6 | 0 |
| C | 2 | 2 | 5 | 1 |
| D | 2 | 5 | 7 | 1 |
| E | 6 | 6 | 9 | 0 |
| F | 5 | 7 | 9 | 1 |
| G | 9 | 9 | 10 | 0 |
The schedule has $T_2=10$. Its integer lower bound is $\max(\lceil17/2\rceil,10)=10$, so this particular schedule is optimal. Its speedup is $17/10=1.7$, efficiency $17/20=0.85$, and idle worker-time is $2\cdot10-17=3$. Worker one is idle for two units at the start and one at the end; neither interval has a ready waiting task.
With one worker, the same DAG takes seventeen units. With two, three, four, or eight workers, the program reports ten: more capacity cannot shorten the A–B–E–G chain. This equality is a property of this sample; a different graph or priority can show different results.
A separate Amdahl comparison. For $\sigma=1/4$, ideal speedup is $4P/(P+3)$. The printed P values give:
| Workers | DAG time | DAG speedup | Amdahl speedup for separate $\sigma=1/4$ model |
|---|---|---|---|
| 1 | 17 | $1$ | $1$ |
| 2 | 10 | $17/10$ | $8/5$ |
| 3 | 10 | $17/10$ | $2$ |
| 4 | 10 | $17/10$ | $16/7$ |
| 8 | 10 | $17/10$ | $32/11$ |
The Amdahl values approach four under their ideal serial/parallel assumptions. They are not a fit to this DAG's span or a prediction of its schedule. All ratios are printed as reduced exact fractions so floating-point formatting cannot obscure cross-language agreement.
Trace It by Hand
Pause just after time five. B is running on worker zero until six, D has started on worker one until seven, and C has completed. Predict which task becomes ready at six and whether F can start then.
Reveal: B completes at six. E's predecessors B and C are both done, so E starts immediately on worker zero and finishes at nine. F still depends on running task D, so it cannot start until seven even though C is already done. At seven, F takes worker one and also finishes at nine. Only after both completions at nine may G begin.
Now examine three independent tasks with durations $[2,2,3]$ on two workers. Minimum-ID priority starts both two-unit tasks first and finishes the last task at five. Starting the three-unit task first permits completion at four. Compute $W=7$, $S=3$, and lower bound four, then explain why being greedy does not prove the first schedule optimal.
Implementation in Python, C++, and Java
Each panel is a complete program with the same weighted DAG, analysis tie rules, event-driven scheduler, Amdahl helper, and output. Run Python normally, compile C++ as C++17, or save the Java panel as Main.java and run Main. The programs simulate workers; they do not spawn actual parallel tasks.
Graph contract. Call run(durations,edges,processors). Durations are a flat integer sequence of length $0\le N\le64$, with each $1\le d_v\le1000$. Each directed edge is an ordered pair of task IDs in $[0,N)$. At most $4096$ edges may be supplied; self edges and duplicates are rejected. Require $1\le P\le64$. Node IDs need not already be topological. Inputs remain unchanged because sorting and indegree updates use copies.
Validation order is processors (PROCESSORS), excessive node count (SIZE), invalid durations (DURATION), excessive/malformed/out-of-range/self/duplicate edges (EDGE), then a genuine directed cycle (CYCLE). Python requires exact integer durations, endpoints, and processor count, rejecting booleans/floats/strings. Its edge rows may be lists or tuples of length two. C++ uses vector<int> durations and vector<pair<int,int>> edges; Java uses int[] and two-element int[][] rows. Non-null outer containers are expected; Java explicitly rejects a null or wrong-length edge row as EDGE.
The $64\cdot1000$ work bound keeps event times and counters small. Empty graphs return zero work/span/makespan, empty assignment arrays, and no events. The program's sample metrics operate on a nonempty graph; speedup, efficiency, and parallelism ratios are undefined for the empty graph and must not be formed as $0/0$.
Analysis output. analyze returns successor lists, original indegrees, canonical topological order, earliest starts/finishes, chosen parents, one critical path, work, and span. For equal longest predecessor finishes, the smaller predecessor ID wins. For equal span endpoints, the smallest ID wins. These choices affect which equally valid critical path is displayed, not its length.
Simulation output. Every event contains its time, sorted completed IDs, sorted ready IDs before dispatch, IDs started in worker-assignment order, waiting IDs after dispatch, and running IDs in worker order. Per-node actual starts, finishes, and assigned workers are also returned. The notation u@w->f in standard output means task u is assigned to worker w and finishes at time f. All intervals are half open, so a worker completing a task at time t can begin another at the same t.
Exact Amdahl helper. amdahl(s,d,P) represents serial fraction $s/d$ and returns reduced speedup $$ \frac{dP}{sP+d-s}. $$ Require integers $1\le d\le10^6$, $0\le s\le d$, and $1\le P\le64$; violations give AMDAHL. C++/Java use 64-bit intermediate products, while Python uses arbitrary-precision integers. The helper treats fractions zero and one correctly and is independent of the graph simulator. The generic fraction formatter is internal and expects a positive denominator in all program uses.
Expected standard output, identical in all three languages:
durations=[2,4,3,2,3,2,1] processors=2work=17 span=10 parallelism=17/10topo=[0,1,2,3,4,5,6] critical=[0,1,4,6]earliest=[0,2,2,2,6,5,9] earliest_finish=[2,6,5,4,9,7,10]t=0 completed=[] ready=[0] start=[0@0->2] waiting=[] running=[0@0->2]t=2 completed=[0] ready=[1,2,3] start=[1@0->6,2@1->5] waiting=[3] running=[1@0->6,2@1->5]t=5 completed=[2] ready=[3] start=[3@1->7] waiting=[] running=[1@0->6,3@1->7]t=6 completed=[1] ready=[4] start=[4@0->9] waiting=[] running=[4@0->9,3@1->7]t=7 completed=[3] ready=[5] start=[5@1->9] waiting=[] running=[4@0->9,5@1->9]t=9 completed=[4,5] ready=[6] start=[6@0->10] waiting=[] running=[6@0->10]t=10 completed=[6] ready=[] start=[] waiting=[] running=[]makespan=10 lower_bound=10 greedy_upper=27/2 speedup=17/10 efficiency=17/20p=1 dag_time=17 dag_speedup=1/1 amdahl_serial=1/4 amdahl_speedup=1/1p=2 dag_time=10 dag_speedup=17/10 amdahl_serial=1/4 amdahl_speedup=8/5p=3 dag_time=10 dag_speedup=17/10 amdahl_serial=1/4 amdahl_speedup=2/1p=4 dag_time=10 dag_speedup=17/10 amdahl_serial=1/4 amdahl_speedup=16/7p=8 dag_time=10 dag_speedup=17/10 amdahl_serial=1/4 amdahl_speedup=32/11
Complexity, Correctness, and Invariants
Laws and greedy guarantees. Any legal P-worker execution needs $T_P\ge W/P$ by processor-time capacity and $T_P\ge S$ by precedence. A work-conserving greedy list schedule on identical workers with zero overhead also satisfies the refined upper bound $$ T_P\le\frac WP+\left(1-\frac1P\right)S =\frac{W+(P-1)S}{P} \le\frac WP+S. $$ The sample prints $27/2$ for the refined two-worker upper bound, while its actual makespan is ten. The upper bound is a guarantee, not a runtime formula. The work/span scheduling treatment in CMU's parallel-computing notes explains the ideal greedy principle and why slackness matters.
One way to see the refined bound is to trace backward from the final finishing task, choosing the predecessor that completed last in the actual schedule. This produces a valid dependency chain of total duration $L\le S$. Before each chain task begins, any gap after its chosen predecessor finished occurs while that task is ready. Greediness forces all P workers to be busy throughout those gaps. During the chain's own execution, at least one worker is busy. Thus $W\ge P(T_P-L)+L$, which rearranges to $T_P\le W/P+(1-1/P)L$ and hence the bound above. The chain used in this proof need not equal the critical path returned by the unlimited-worker analysis.
Combining upper and lower bounds gives a constant-factor guarantee; the refined form is at most $(2-1/P)$ times optimal for this model. It does not prove a particular priority order optimal. When $P\ll W/S$, the additive span term is small relative to $W/P$, so ideal greedy speedup can approach linear scaling. When $P\gg W/S$, span limits further benefit. Large parallelism is useful only if the algorithm also avoids excessive work compared with its sequential competitor.
Correctness of work/span analysis. The ready heap contains exactly the unprocessed tasks with no unprocessed predecessor. Removing a root of that remaining graph preserves topological validity. When a task is processed, every predecessor finish is final; the maximum yields the earliest feasible start. Induction along topological order proves every computed ES/EF and the maximum span. Parent links attain the selected maxima, so their recovered chain has total duration S. Summing each duration once proves work. Processing fewer than N vertices means a cycle remains and no valid topological execution exists.
Correctness of the simulation. Each task is in exactly one state: not started, running on one worker, or completed. A not-started task's remaining predecessor count equals the number of its predecessors not yet completed, including running ones. Only a transition to zero enqueues it, and it is removed exactly once at dispatch. A dispatched task occupies one worker until its recorded finish, so no worker intervals overlap and no task is started twice.
At an event, finish all tasks ending now before dispatch. Every new start therefore has all predecessor finishes at or before its start. The scheduler fills every free worker while the ready queue is nonempty. Positive durations make the next running finish strictly later; acyclicity guarantees progress until every task completes. These invariants prove a legal deterministic greedy schedule. They do not establish race freedom for a real program unless the supplied DAG captures all required data dependencies.
The analyzer's CPU cost differs from W. Let N count tasks and M edges. Sorting the copied edges for validation/canonicalization costs $O(M\log(M+1))$. Graph construction and the dynamic program scan all edges; the minimum-ID topological heap inserts/removes each node once, costing $O(N\log(N+1))$. The general work/span analysis can be linear with an ordinary queue, but its canonical minimum-ID order here costs a heap factor.
The event simulator has at most $N+1$ events because every event after time zero completes at least one task. It scans P worker slots and copies/sorts ready-state snapshots at each event. Each node is dispatched once and each edge is released once. Including analysis, a safe total host-runtime bound is $$ O\bigl(M\log(M+1)+N^2\log(N+1)+NP+N+M+P\bigr). $$ Snapshot storage can reach $O(N^2)$; graph/analysis/assignments use $O(N+M)$, and running-worker storage $O(P)$. Overall host space is $O(N^2+N+M+P)$. These are simulator implementation bounds. They are not the ideal task computation's $W$ work or S span, and software simulation time is not $T_P$.
Best/average/worst qualifications. There is no task-order probability distribution, so no universal average makespan is asserted. For a fixed graph, W and S are fixed; scheduling priority and processor count affect actual makespan. Chains have $T_P=W=S$ for all P. Independent unit tasks have $T_P=\lceil N/P\rceil$, span one, and work N. The host analyzer still reads the graph in both cases; simultaneous completions can reduce its number of recorded events. The Amdahl helper performs bounded integer arithmetic and Euclidean fraction reduction, not parallel execution.
| Computation or method | Work / CPU task demand | Span / dependency limit | Interpretation |
|---|---|---|---|
| N-task unit chain | $\Theta(N)$ | $\Theta(N)$ | More workers cannot shorten the chain |
| N independent unit tasks | $\Theta(N)$ | $1$ | Greedy time $\lceil N/P\rceil$ with enough ready work |
| Balanced binary reduction | $\Theta(N)$ | $\Theta(\log N)$ | Work-efficient grouping exposes logarithmic-depth dependencies |
| Work-efficient parallel prefix scan | $\Theta(N)$ | $\Theta(\log N)$ | Extra structure supports all prefixes without quadratic work |
| Mergesort with serial merges | $\Theta(N\log N)$ | $\Theta(N)$ | Parallel recursive halves alone leave a long merge bottleneck |
| Mergesort with linear-work logarithmic-span merges | $\Theta(N\log N)$ | $\Theta(\log^2N)$ | Requires a suitably parallel merge, not just two spawned recursive calls |
| Fixed-fraction Amdahl model | Same fixed workload | Serial time $\sigma T_1$ plus ideally divided parallel time | Ideal speedup $1/(\sigma+(1-\sigma)/P)$ |
| Supplied DAG analyzer/simulator | Graph/heap/snapshot software costs stated above | Does not execute graph tasks in parallel | Produces a verifiable schedule under an ideal model |
Topological induction makes each earliest finish the longest weighted path ending at that task. During simulation, a task becomes ready only after all predecessor completions, starts once, and occupies one worker until its finish. Those invariants establish the span and a legal greedy schedule; optimality requires an additional argument such as matching a lower bound.
Variants, Pitfalls, and Optimizations
Do not replace bounds with equalities. $\max(W/P,S)$ is a lower bound and $W/P+(1-1/P)S$ a greedy upper bound. Neither is generally the actual runtime. Even indivisible independent tasks of durations $[2,2,2]$ on two workers need four units, while the integral work/span lower bound is three. The graph's task granularity matters.
Greedy priority can change makespan. In the diagram's separate example $[2,2,3]$, smallest-ID scheduling takes five units; starting the three-unit task first permits four. Both dispatch whenever a task can run. A longest-ready-task or bottom-level priority can be useful to explore, but a heuristic needs its own evaluation and does not solve every precedence-constrained scheduling instance optimally.
Distinguish graph time from worker waiting. ES/EF describe unlimited workers. The sample's D could start at time two but actually starts at five; F's earliest start is five but actual start is seven. Replacing ES with actual start during the span dynamic program would make a supposed graph invariant depend on a particular P-worker schedule.
Use all predecessors, and complete before releasing. Counting a task as finished when dispatched can start a child too early. Requiring only one of several parents can also violate a join. Duplicate edges can corrupt indegree bookkeeping if the builder counts them differently from the releaser. This code rejects duplicates and processes all same-time completions before applying ready-ID priority.
Weight the graph in consistent units. Longest path by edge count is span only for an appropriate unit-task convention. Node count alone misses durations, communication, or weighted costs. If communication is essential, model it as explicit tasks/dependencies or a justified edge-delay extension. Do not mix microseconds, instruction counts, and bytes in one path sum.
Keep the baseline explicit. Work-based speedup compares this DAG's one-worker cost W with its P-worker cost. An algorithm with twice the sequential work may scale nicely yet still lose to a better serial algorithm. Work efficiency compares the parallel algorithm's W with the best relevant sequential complexity. Real measured speedup can exceed P because of changed memory behavior or baselines; that lies outside the fixed-cost model's work law.
Amdahl needs a fixed problem and serial fraction. The formula assumes the serial duration stays unchanged, the parallel remainder divides perfectly, and added overhead is zero. The fraction must come from the same one-processor baseline. Simply setting $\sigma=S/W$ generally double-counts a portion of the critical path as globally serial. Scaling the problem as P grows asks a different question; distinguish fixed-size strong scaling from scaled-workload experiments.
Granularity trades overhead against parallelism. Splitting a large task can expose more ready work and reduce indivisibility, but creates scheduling, allocation, and synchronization costs. Keeping tasks too coarse can hide available parallelism; making them too fine can inflate W. Represent essential spawn/join work or measure it when moving beyond the ideal DAG.
Real resources add constraints. Memory bandwidth, caches, NUMA placement, network transfers, locks, and worker heterogeneity can make durations depend on concurrent activity. A deterministic zero-overhead simulator cannot predict those interactions. Profiling actual code should separate task work, waiting, scheduler activity, and communication, then compare with the graph's structural lower bounds.
A valid graph is an assumption about the program. Missing a read/write dependency can produce a fast simulated schedule that is unsafe to execute. Adding an unnecessary dependency preserves safety but reduces modeled parallelism. Validate both directions of the graph construction: every required ordering is present, and artificial serialization has a reason. Nonpreemptive tasks here also have no internal race or resource model.
Handle empty graphs and ties deliberately. Empty W/S and speedup ratios are undefined, even though empty makespan is zero. Equal predecessor finishes can give several critical paths. IDs are only deterministic tie-breakers, so relabeling independent tasks can change the priority schedule while leaving work/span unchanged. Different topological orders are valid; compare canonical outputs only when the tie conventions match.
Keep work, span, limited-worker makespan, and the software analyzer's runtime separate. Respect every predecessor completion and make priority/tie rules explicit. A lower bound, greedy guarantee, fixed-fraction Amdahl curve, or favorable sample schedule becomes a runtime claim only under its stated assumptions.
Practice Problems and Real-World Use
Use independent checks for the graph analysis and scheduling logic. A predecessor-based memoized DFS can compute longest weighted paths independently of the implementation's Kahn traversal, while a unit-time reference simulator can verify its event jumps. Compare the entire schedule, not just final runtime: every completion batch, ready set, dispatch, waiting set, running set, and assignment interval should agree.
Check that every task starts and completes exactly once, no worker's intervals overlap, every dependency satisfies finish[u] <= start[v], interval length equals duration, and both work/span lower bounds hold. With one worker, makespan must equal W; with at least N workers, it must equal S. Check the greedy upper bound too, including cases where it is loose or attained. Reordering the same input edge list must not alter the canonical result.
All three implementations passed $3575$ shared inputs per language: $2414$ graph workloads and $1161$ Amdahl cases. The graph suite has $2398$ valid cases and $16$ rejected cases; the Amdahl suite includes $8$ rejected cases. It covers exhaustive forward-edge DAGs through four vertices, randomized relabeled DAGs, chains, independent tasks, fan-in/fan-out, equal-time joins, empty graphs, maximum node/duration/processor limits, invalid edges/cycles, and the separate granularity/priority counterexamples. Independent DFS and integer-tick oracles match every analysis field, assignment, event, and makespan. Exact rational arithmetic checks the Amdahl helper, including serial fractions zero/one and large denominators. Additional checks cover $29$ Python type/edge-shape rejections and $75$ input-edge permutation comparisons. C++ ran with address and undefined-behavior sanitizers; all sample output matches byte for byte.
For algorithm design, start by writing a dependency decomposition and estimating work/span before tuning worker count. Then improve the expensive term: reduce redundant work, shorten a long dependency chain, or expose enough tasks to keep workers busy. Compare ideal improvements with measured scheduler and memory costs.
Applications include a build graph with compilation/link dependencies, a numerical reduction tree, a parallel prefix primitive, or a data-processing pipeline. Their real cost models differ, but each benefits from separating computation volume, mandatory order, and resource-limited execution.
| Exercise or setting | What to implement or verify | Difficulty |
|---|---|---|
| Seven-task hand trace | Reproduce all ES/EF values and the two-worker event table | Beginner |
| Chain versus independent tasks | Compare identical work with different spans and worker utilization | Beginner |
| Amdahl fraction sweep | Compute exact speedup for several serial fractions and limits | Beginner |
| Alternate greedy priorities | Reproduce makespans five and four for durations $[2,2,3]$ | Intermediate |
| Parallel reduction / prefix scan | Derive work and span from the dependency tree | Intermediate |
| Build or data-pipeline graph | Add measured durations and examine the critical chain | Intermediate |
| Parallel mergesort design | Compare serial-merge and parallel-merge span recurrences | Advanced |
| Overhead-aware experiment | Add spawn/communication tasks and compare ideal with measured scaling | Advanced |