Performance Engineering · Optimization Practice
Measuring What a Component Costs by Taking It Away
Ablation measures a component by disabling it and observing the change in a metric, which answers a different question than profiling and is easy to apply incorrectly.
A profiler answers “where did the CPU go?” Ablation answers “what changes if this part is gone?” The distinction looks like a matter of phrasing and is in fact the difference between a partition and a derivative: the profiler attributes cost to time, while ablation attributes cost to the metric being measured, after all the interactions among components have been allowed to play out.
The method is old and simple: disable one component, re-run the workload, measure the change, restore. Its simplicity is deceptive, because the result of an ablation depends on how the component was disabled, on whether the workload still does the same thing afterwards, and on which metric is being watched. Each of these is a place where the measurement can be right in its arithmetic and wrong in its conclusion.
What disabling actually means
There are at least four distinct interventions, and they yield four different numbers for the same component.
Removal deletes the work and everything that consumes its result: the output is not produced and no consumer reads it. This measures the full contribution of the component to the end-to-end path, including the parts of downstream code that existed only to process its output.
Stubbing replaces the component with a constant or a no-op while leaving the call and its consumers in place. This measures the component’s own cost including its interface overhead, but it also measures whatever the consumers do with a degenerate value — frequently nothing, sometimes a great deal, since degenerate values can trigger different code paths.
Replacement substitutes a cheaper but functionally equivalent implementation. This measures the excess cost of the current implementation over the substitute, not the cost of the operation. A cache replaced by a hash map yields the cache’s overhead, not the cost of the lookup it performs.
Delay injection adds a known amount of latency and measures the change. The response to a delay is often nonlinear because of queueing, so the derivative obtained this way is a local sensitivity rather than a total cost, and extrapolating it linearly overestimates the benefit of removing the component entirely.
A published ablation result without the intervention specified is nearly uninterpretable, which is why the intervention belongs in the record alongside the number.
The four interventions differ by more than a label in the common case of a remote call. Replacing a remote cache read with a local hash lookup measures the network round trip and the serialization, but leaves the caller’s control flow intact. Stubbing the call to return a fixed value also skips the deserialization and any validation of the returned object, so the number is larger by the cost of work that removal did not actually target. Removing the call and its call site removes the branch, the error handling, and the retry path as well. The spread between the smallest and largest of those numbers can easily be a factor of two, which is the same order as the difference between an optimization worth doing and one worth skipping.
Sampling error, and the size of the effect to expect
Ablation produces a difference between two measurements, and each of those measurements has error. The error of the difference is the combination of both, so an ablation is less sensitive than a single measurement of the same workload by roughly a factor related to the ratio of the effect to the noise.
The consequence is a budget: to resolve an effect of size $\delta$ on a metric whose run-to-run standard deviation is $\sigma$, the number of repetitions per arm needs to be large enough that $t\sqrt{2\sigma^2/n}$ is comfortably below $\delta$ for a suitable critical value $t$. For a metric that varies by five percent between runs, resolving a one percent effect requires a design with enough repetitions that the standard error is a fraction of a percent — a design substantially bigger than the two-run comparison that ablation is usually implemented as.
import math
def reps_needed(effect, sigma, t=2.0):
"""Repetitions per arm for a two-sample comparison to resolve `effect`."""
n = 2
while t * math.sqrt(2 * sigma**2 / n) > effect:
n *= 2
return n
# Effect 1% of mean 100, run-to-run sigma 5 (5%); enough to see it?
print(reps_needed(effect=1.0, sigma=5.0)) # needs a large n
Amplifying the effect is usually cheaper than increasing the repetitions. A component that is one percent of a ten-second request contributes a tenth of a second, which is easy to see if the workload is shaped to make that component the only source of variation — many repetitions of the same request rather than a mix, a warm cache, a fixed input. This is the same principle as designing for resolution rather than for realism: the measurement’s power comes from the workload’s shape as much as from the number of runs, and a workload that mixes request classes dilutes every per-class effect by the share of that class.
Invariants that must survive the ablation
The most common failure in ablation is that the disabled system no longer does the same work, and the measurement compares two different programs.
Disabling a validation step speeds the request up and changes what the system accepts. Disabling a cache speeds the request down and changes the load on the backing store, which may change the metric through contention rather than through the cache’s own cost. Disabling a compaction or a background task removes its direct cost but lets the thing it maintained grow, and a long enough run charges the growth to the consumers instead. Disabling logging removes both the log write and, in some systems, the serialization and formatting of the record, and it also removes the I/O pressure that shaped every other component’s timing.
The invariant to state explicitly is which properties of the run must be held fixed: the set of accepted requests, the state of the backing store, the amount of memory in use, the number and rate of concurrent clients. An ablation is valid only if these are the same in both arms, which usually means the disabled run must be shorter than the point at which divergence appears, or must be paired with a compensating action that restores the invariant.
Working with the interaction term
Components in a real system are not additive, so ablating two of them does not produce the sum of their individual ablations. A cache and a lock interact: removing the cache raises the request rate to the backing store, which raises contention on the lock, so the combined ablation is larger than either alone. Removing both the cache and an unrelated validator produces exactly the sum of their individual effects only if they share no resource and no causal path.
This is the ordinary mathematics of interaction, and it means the ablation should be applied at the level where decisions are made. Ablating a leaf function measures the function; ablating a subsystem measures whatever the subsystem would be replaced by, including the interactions the replacement would dissolve. If the plan is to replace the subsystem, the subsystem-level ablation is the relevant one, even though it is a coarser number.
What ablation answers that a profile cannot
Three questions are answered by ablation and not by attribution of cost.
Which components extend the critical path. A component that fills slack has a large profile share and a zero ablation effect on latency. Running the ablation against the latency metric identifies the components whose removal changes the end-of-request time, which is the property the critical path analysis formalizes.
What a candidate would actually return. The predicted saving from expected-payoff ranking is an estimate; the ablation is the measurement of the same quantity, taken before the implementation exists. Treating the ablation as the ceiling — the number that the implementation cannot beat because it assumes the component vanishes entirely — is the standard way to check an optimistic plan.
Whether a component’s cost is structural. A component whose ablation effect does not change when the workload changes is paying a fixed structural cost; one whose effect scales with load is paying a cost that interacts with concurrency. The second is the more dangerous kind, because it is invisible at low load and grows as the system saturates, and because a queueing delay is not attributable to the component whose service time produced it.
Running an ablation on a system that cannot be branched
The clean form of the method assumes two builds that differ in one place. Many systems cannot supply that: the build is a single artifact, the deployment is continuous, and a temporary fork of the code is unacceptable. Three substitutions preserve most of the value.
In-process toggles are the most direct. A configuration flag that bypasses a component for a fraction of traffic makes both arms observable simultaneously, which removes cross-run variation from the comparison entirely: the two arms share hardware, scheduler state, and traffic mix, and the difference is measured within the same window. The cost is that the flag itself must be evaluated on every call, and that a flag left in place after the experiment is a branch that will eventually be taken by the wrong configuration. The experiment has a defined end date; the flag should have one too.
Replayable traces are the second substitution. If the request stream can be recorded and replayed against two builds, the two arms see identical input, which removes input variation and leaves only the system’s own nondeterminism. This is stronger than a synthetic load generator because it reproduces the distribution that actually matters, including its expensive outliers, and it is the same machinery that defining an objective for a workload mix requires.
Instrumented timing in place of removal is the third. Where neither a toggle nor a replay is available, the component’s duration can be measured directly and its share of elapsed time computed, which estimates the ablation from below: it captures the component’s own time but not the downstream work that its output caused. That gap is exactly the difference between the “stubbing” and “removal” interventions, and knowing that the estimate is a lower bound is enough to keep it from being mistaken for the total.
The record of an ablation should include which of these substitutions was used, because a lower bound from instrumented timing and a total from removal are not comparable quantities even when they are measured on the same component.
References
- The Design of Experiments — Ronald A. Fisher, Oliver and Boyd, 1935
- Systems Performance: Enterprise and the Cloud — Brendan Gregg, Addison-Wesley
- The Art of Computer Systems Performance Analysis — Raj Jain, Wiley
- Performance Modeling and Design of Computer Systems: Queueing Theory in Action — Mor Harchol-Balter, Cambridge University Press