Computer Science Computer Science major 100% Free Open Access
Chapter 8 • Theory & Derivations

Optimization and reliability

Profiling; Load Balance; Race Diagnosis; Reproducible Parallel Results

§8.1 Profiling

Profiling is the disciplined process of measuring where execution time and resources are consumed. A program that uses sixteen threads is not necessarily performing sixteen independent units of useful computation. Some threads may be blocked on locks, waiting at barriers, suffering memory stalls or competing for a shared device. A useful parallel profiler distinguishes active computation, synchronization waits, communication, I/O and runtime scheduling. CPU sampling can identify frequently executed code paths; event-based profilers can report cache misses, bandwidth or vector utilization; GPU profilers can show kernel duration, occupancy and memory transactions. Each instrument has overhead and blind spots, so interpret measurements alongside the underlying program semantics.

Begin with a valid baseline. Record the input problem, processor or GPU configuration, compiler options, runtime library versions and output checksum. Time the whole program, then isolate expensive phases. If a simulation spends half its time reading files and half computing, doubling compute throughput can at best reduce the total time to three-quarters of its original value while file time remains unchanged. Optimizing a tiny hot-looking function outside the critical path may have almost no effect. The first profiling question is what the user is waiting for, not which line has the largest number of instructions.

Sampling profilers observe where execution is occurring at periodic intervals, giving a statistical estimate of time distribution. They can miss very short events or misattribute time when inlining and compiler optimization obscure source locations. Instrumentation inserts measurement events around known regions and can provide more precise phase durations, but it may perturb execution, especially when many threads contend on the logging system. Hardware counters report events such as retired instructions, branch misses and cache activity, though multiplexing, counter scope and architecture differences matter. A high cache miss count does not automatically prove memory-bound behavior: miss penalties may overlap or be hidden by other work.

Parallel timelines reveal patterns that aggregate timings conceal. A long horizontal gap before a barrier may show that one worker performed more computation than its peers. A series of tiny GPU kernels separated by host synchronization can reveal launch-bound execution. A communication timeline may show a rank sending many small messages while others remain idle. Annotating application phases and work identities improves interpretation. Without identifiers, one cannot tell whether a delay came from expected dependency, contention, or a task that was never enqueued.

The roofline model compares arithmetic intensity with machine ceilings. Estimate useful arithmetic operations per byte of data moved from the memory level being modeled. A kernel with two arithmetic operations for every eight bytes transferred has intensity 0.25 operations per byte. If a system sustains 100 GB/s of relevant bandwidth, that intensity suggests a memory-bound ceiling of about 25 billion operations per second before considering other limits. If the same kernel is measured at two billion operations per second, the gap invites investigation of inefficient access, serialization or insufficient concurrency. The roofline is a bound, not an exact prediction, and its interpretation requires accurate operation and byte counts.

A scalable profiling protocol sweeps both problem size and worker count. Fixing only one input can miss a regime change: small problems may be launch-bound, medium problems cache-friendly, and large problems bandwidth-bound. Repeated runs reveal noise from scheduling, frequency scaling and shared system activity. Use medians and dispersion instead of reporting a single best time. When comparing two implementations, ensure equivalent results, compilation flags and resource allocation. A ten-percent improvement smaller than run-to-run variation is not a robust finding without further evidence.

Performance diagnosis should produce a falsifiable hypothesis. If one believes a kernel is limited by memory traffic, estimate the bytes, compare with measured bandwidth and test whether improving locality changes runtime. If a barrier seems responsible, remove or restructure it only after proving the dependency remains satisfied, and then measure idle time again. If process placement is suspected, control affinity and compare local versus remote memory access. A profiler is most valuable when it guides an experiment rather than merely displaying attractive charts.

Finally, record performance results as reproducible research artifacts. Include benchmark scripts, input generators, environment versions, raw measurements and definitions of derived metrics. Do not claim an optimization was verified on hardware that was not available. A simulated performance model can be useful for teaching, but it must be labeled as a model and validated against real measurements before informing a hardware purchase or production capacity decision.

§8.2 Load Balance

Load balance concerns how evenly useful work is distributed among workers and whether those workers can operate independently without exhausting shared resources. Equal task counts are a poor proxy when tasks have different costs. A process might own ten matrix blocks with dense arithmetic, while another owns ten blocks that are mostly zero. Their nominal task counts match, but execution times do not. A simple diagnostic records per-worker useful computation time and compares average to maximum. If four workers take 4, 4, 4 and 12 seconds, the average is six, the maximum is twelve, and an elementary balance efficiency is six divided by twelve, or fifty percent. The imbalance explains idle time at the next synchronization point.

The theoretical makespan for independent tasks is at least the largest task duration and at least total work divided by the number of processors. Suppose six jobs take durations [2,2,2,2,2,12] seconds on three identical workers. Total work is 22 seconds, so the average bound is 7.33 seconds, but the twelve-second job imposes a stronger lower bound of twelve. Assigning the long job to one worker and splitting the five short jobs among the others can achieve twelve seconds. No clever scheduler can beat that without splitting the long job itself. This reasoning prevents blaming a runtime for an unavoidable critical path.

Static partitioning is inexpensive and locality-friendly when work costs are predictable. It can perform badly on irregular datasets or when input behavior changes. Dynamic work queues assign new chunks to idle workers, helping with unpredictable durations. The price is queue contention, scheduling overhead and potential loss of cache affinity. Work stealing gives idle workers an opportunity to take tasks from busier peers, often benefiting recursive divide-and-conquer algorithms. A scheduler must also consider memory locality: stealing a task that repeatedly reads remote NUMA pages can be slower than waiting briefly for a local task.

Weighted partitioning estimates work before execution. For a sparse matrix, the number of nonzero entries per row may be a useful weight; for graph traversal, vertex degree may approximate edge-processing cost. Those proxies can still mispredict execution if cache locality, branching or expensive user-defined functions dominate. Adaptive profiling can update estimates between iterations. But collecting detailed cost statistics also consumes time and memory. The relevant decision is whether better assignment saves enough repeated work to justify the measurement and migration overhead.

Load balancing must include resource constraints. Assigning two memory-heavy tasks to the same worker might exceed available RAM even if their arithmetic durations appear short. Placing all communication-heavy tasks on one network endpoint creates contention despite balanced CPU cycles. GPU tasks can compete for memory capacity and execution streams. A heterogeneous cluster contains machines with different compute rates or memory bandwidth; assigning equal work counts may be inherently unbalanced. Weighted capacity and performance measurements should guide task placement.

Imbalance can arise from synchronization patterns too. When workers finish local tasks at different times but must enter a barrier, the fast workers idle. Eliminating the barrier may improve overlap only if the next phase's dependencies permit it. In a stencil simulation, advancing one worker's time step before its neighbors have produced required halos can change the numerical method. In a task graph, replacing global barriers with fine-grained dependencies can preserve correctness while admitting more concurrency, but the dependency tracking itself consumes resources. Optimize the program's critical path, not simply the count of barriers.

Rebalancing an evolving simulation involves state migration. A partition that becomes overloaded might donate cells to another rank, but transferring state and rebuilding neighbor tables is expensive. If migration costs twenty seconds and the new partition saves two seconds per future iteration, it breaks even after ten iterations in the simplest model. If only five iterations remain, the migration is counterproductive. Forecasting future workload is uncertain, so practical systems use thresholds and moving averages rather than reacting to every transient spike.

A good load-balance report includes maximum, average and distribution of per-worker useful time; idle time; movement costs; task size distribution; and total wall-clock improvement. It also documents the partitioning rule and whether the workload is deterministic. The final objective is not cosmetically equal load bars but the shortest reliable completion time at acceptable resource cost.

§8.3 Race Diagnosis

A race condition exists when a program's behavior depends on the relative timing of operations that are not adequately ordered. A data race has a more specific meaning under a language memory model: conflicting memory accesses occur without the required synchronization and at least one is a write. Not all race conditions are data races. Two correctly synchronized workers might each legitimately win a contest to reserve a shared resource, making the business outcome depend on schedule even though every memory operation is properly protected. A diagnosis must identify whether the bug violates memory safety, an ordering invariant or an application-level decision rule.

The simplest lost-update example uses a shared integer counter initially zero. Thread A reads zero and computes one; thread B reads zero and computes one; both store one. The intended result after two increments is two, but the observed value is one. The fault is that an increment is a read-modify-write sequence rather than one indivisible operation. Protecting the operation with a mutex or using a suitable atomic fetch-add repairs the counter. For a large histogram, however, millions of contending atomic updates may be slow. A better design may use private histograms and a final reduction, which improves both clarity and throughput.

A race diagnosis begins with an invariant. For the counter, expected value equals the number of completed increments. For a producer-consumer queue, every enqueued item must be consumed at most once, and a completed item must not disappear. For a numerical solver, outputs must correspond to one coherent input time level. Instrument tests to check the invariant on small inputs with exaggerated opportunities for interleaving. For example, insert test-only yield points after the read and before the write. Such perturbations can reveal a bug, but they are not a synchronization mechanism and their absence cannot prove correctness.

Thread sanitizers analyze memory accesses during execution and can detect many conflicting unsynchronized operations. Their reports need context: custom synchronization, foreign libraries and unsupported language features may affect accuracy. A lock-order checker can identify cycles that create deadlock risks. GPU race-detection tools can inspect certain shared-memory or global-memory hazards, but tool coverage varies. Use dynamic tools as evidence, not as substitutes for a clear argument about who owns each memory location and how ownership changes.

Memory visibility often hides behind an apparently innocent flag. A producer fills data and sets ready, while a consumer polls the flag and then reads the data. If ordinary unsynchronized variables are used where the language requires atomics or locks, the compiler and CPU may reorder accesses or the consumer may observe stale values. Introducing an arbitrary sleep is not a fix. A release publication and acquire observation, or a mutex and condition variable, can establish the necessary happens-before relation. The precise primitives depend on the programming language and device scope.

Atomicity alone does not guarantee a correct high-level protocol. Two workers might each check a shared balance and then decrement it using individually atomic operations, resulting in an overdraft if the check and update are not one transaction. Compare-and-swap loops, locking or a serialized owner can protect the combined invariant. Similarly, an atomic flag used to mark a vertex visited in parallel BFS must be paired with a rule that only the successful claimant enqueues the vertex when that property is needed. The data structure's logical operation, not the individual load or store, defines the required atomicity boundary.

Reproducing failures requires care. Running the same test ten thousand times can increase confidence that a particular bug is rare under those conditions, but it cannot exhaust all thread schedules. Model checking or systematic schedule exploration can prove properties for small bounded models, subject to the model's assumptions. Formal reasoning from data ownership and synchronization remains important for the full implementation. A particularly useful review practice is to list every shared mutable location and ask which writes may overlap, which reads depend on those writes, and what establishes ordering.

A race fix must be retested for performance. One coarse mutex might eliminate the race while serializing all workers, making the application unusable. Finer-grained locks, immutable snapshots or disjoint ownership can preserve more parallelism. The best solution often changes the dataflow so that conflicting accesses no longer exist. Correctness first, then measured optimization, is essential because a spectacular speedup produced by dropped increments or missing synchronization has no engineering value.

§8.4 Reproducible Parallel Results

Reproducibility asks whether a computation can be repeated with meaningfully equivalent results under a stated set of conditions. For integer algorithms with defined overflow behavior, strict deterministic scheduling may be unnecessary if operations are associative and independent. For floating-point numerical work, different execution orders can alter rounded values. For stochastic algorithms, random streams and data-order choices affect outputs. For asynchronous graph or optimization algorithms, the order of updates can affect convergence paths. A reproducibility policy therefore needs to specify its level: bitwise identity, tolerance-based numerical agreement, statistical equivalence, or agreement on a scientific conclusion.

Floating-point addition illustrates the issue. The result of (a+b)+c can differ from a+(b+c) due to finite precision. A parallel reduction tree groups values differently from a serial left-to-right accumulation. The program may be mathematically correct while producing slightly different last bits. If the application compares two large simulation runs, it should use error norms, conservation properties or a scientifically justified tolerance, not demand bitwise identity without explaining why. Some applications, such as audited financial calculations or regression test baselines, may nevertheless require exact deterministic outputs and should select algorithms and arithmetic representations accordingly.

Random-number generation must be parallelized deliberately. Giving every worker the same seed duplicates streams and can drastically reduce the effective sample diversity in Monte Carlo estimation. Assigning seeds based only on scheduling order can make results change whenever task placement changes. Counter-based generators or explicit stream identifiers tied to stable simulation entities can make results reproducible regardless of worker count, when used correctly. Record generator family, seed scheme, stream assignment and library versions. For privacy- or security-sensitive randomness, statistical simulation generators must not be mistaken for cryptographically secure random sources.

Deterministic task assignment can help debugging but may reduce load-balancing flexibility. A fixed schedule and reduction tree make execution easier to replay, yet irregular workloads can leave resources idle. Some systems offer a deterministic mode for validation and a faster nondeterministic mode for production. This is acceptable when both modes satisfy the same application-level correctness and tolerance criteria. The distinction should be documented in benchmark reports, because comparing a deterministic serial baseline against a relaxed parallel version can hide numerical or semantic differences.

Versioned inputs and environments are as important as execution order. A result depends on source data, compiler options, mathematical libraries, hardware instructions, precision modes and solver tolerances. A scientific pipeline should record input checksums and parameter files, not only code commit identifiers. Replaying a computation with changed library defaults or GPU kernels may produce different numerical outputs even from identical source code. Where practical, container images or environment manifests reduce ambiguity, but they do not freeze hardware behavior or external service responses.

Parallel checkpoints require consistent state. If some workers save after time step k while others save after k+1, the combined files may represent no valid global state. A coordinated checkpoint protocol or versioned application-level snapshot must establish a coherent cut. Each file should include rank or partition identity, iteration, dimensions and checksum. The recovery process must validate the complete set before continuing. Atomic publication of checkpoint metadata can prevent users from mistaking partially written files for complete recovery points.

Academic verification combines several independent tests. Compare small parallel outputs to a trusted serial reference; check invariants such as conservation and permutation preservation; vary thread counts and partition boundaries; and repeat stochastic runs to compare statistical properties. For a floating-point solver, a residual or error norm can be more informative than equality of raw intermediate arrays. For nondeterministic graph traversal, several valid visitation orders may exist while final distances remain uniquely defined. The acceptance criterion must match the algorithm's semantics.

Reproducibility is not synonymous with suppressing every form of nondeterminism. It means making claims that can be independently checked given declared assumptions and tolerances. A reproducible result comes from controlled inputs, explicit algorithmic contracts, adequate test evidence and transparent recordkeeping. Those practices make parallel programs trustworthy even when their execution schedules vary from run to run.

THREE WORKED QUESTIONS

Step-by-Step Programming Fundamentals Solutions

Three original questions connect calculations, definitions and interpretation. Open each solution to follow the reasoning.

Solved Problem Example 8.1: Evaluate a load-balance trace

Four workers have useful times [4,4,4,12] seconds and synchronize at one barrier. Compute an elementary balance efficiency and the possible idle time.

Compute mean and maximum

The mean useful time is (4+4+4+12)/4=6 seconds, and the slowest worker needs 12 seconds.

Estimate efficiency

Mean/max=6/12=0.5, or 50 percent. If workers start together, the three short workers each wait about eight seconds at the barrier.

Identify improvement

Repartition the long task if divisible; removing the barrier without preserving the phase dependency would risk correctness. Measure actual scheduling and migration overhead before claiming improvement.

Solved Problem Example 8.2: Analyze a counter race

Two threads each execute a non-atomic read-increment-write on counter zero. Construct an interleaving that leaves the result at one rather than two.

Read concurrently

A reads zero and stores the value in its local register. Before A writes, B also reads zero and computes one.

Perform writes

A stores one and B stores one. The final shared value is one, although two increments were logically requested.

Repair the invariant

Use an atomic fetch-add, a properly scoped lock or thread-local counters plus a reduction. Sleep calls are not valid synchronization and cannot prove correctness.

Solved Problem Example 8.3: Decide whether to repartition

A repartition costs 20 seconds upfront and saves two seconds on each future iteration. Calculate the break-even iteration count and decide whether to repartition if only five iterations remain.

Compute break-even

At two seconds saved per iteration, recovering a 20-second migration cost requires 20/2=10 future iterations.

Evaluate five iterations

Five iterations save only ten seconds, less than the 20-second migration expense; the simple net effect is ten seconds slower.

State uncertainty

Prediction error, changing workload and one-off communication costs can alter the decision. Monitor imbalance over time and test that migrated state remains complete and correctly owned.