Elimination, and the swap

The swap that is not optional

Run elimination without a row interchange on a matrix that needs one and nothing announces a failure. There is no division by zero, no warning, and an answer of the right shape. It is simply wrong, and how wrong depends on a number you did not look at.

Here is a two-by-two system. The matrix has ε in its top-left corner and ones everywhere else; the right-hand side is (1, 2). Its exact solution, for any small ε, is x₁ = 1/(1−ε) and x₂ = (1−2ε)/(1−ε), which for ε = 10⁻¹⁷ is (1.000000, 1.000000) to every digit anyone would print.

Solve it by Gaussian elimination without a row interchange. The answer that comes back has 1 in its first component and 0 in its second.

Nothing failed. The pivot ε is nonzero, so there was no division by zero. No exception was raised, no condition was flagged, and the answer is a perfectly ordinary pair of numbers. It is just not the answer.

Elimination with and without pivoting, ε = 10⁻¹⁷The same two-by-two system solved twice. With a row swap the answer is exact; without one the upper triangular factor contains an entry of order one over epsilon and the second component of the answer is wrong.[ ε 1 ; 1 1 ] x = [ 1 ; 2 ], exact answer (1.000000, 1.000000)with partial pivoting1101U after elimination1.0000001.000000computed xbackward error 0forward error 0without10⁻¹⁷10-1·10¹⁷U after elimination0.0000001.000000computed xbackward error 0.25forward error 0.71no error is raisedgrowth 10¹⁷
Fig. 1 The same system solved twice. On the left, with the row swap: U has entries of size 1, the answer is right to sixteen digits, and the backward error is zero. On the right, without: U contains 1 − 1/ε, a number of size 10¹⁷, and the second component of the answer is wrong. Drag ε and watch the failure appear gradually, with nothing marking the point where the answer stops being usable.

What went wrong, precisely

The multiplier is 1/ε = 10¹⁷. Eliminating the second row subtracts 10¹⁷ times the first row from it, so the (2,2) entry becomes 1 − 10¹⁷ and the right-hand side becomes 2 − 10¹⁷.

Both of those are computed correctly. But 1 − 10¹⁷ is, in double precision, exactly −10¹⁷: the 1 is below half the gap between representable numbers at 10¹⁷, which is 16. So the information carried by that 1 — which is the entire content of the second equation — is gone. Likewise the 2.

Back-substitution then computes x₂ = (−10¹⁷)/(−10¹⁷) = 1, and x₁ = (1 − x₂)/ε = 0/ε = 0. The algorithm has solved, exactly, a system in which the two ones were absent.

Two things about this are worth separating. The subtraction 1 − 10¹⁷ is correctly rounded and introduces the smallest error it is permitted to. What destroyed the answer is that the operands were sixteen orders of magnitude apart, and they were sixteen orders apart because a multiplier of 10¹⁷ was used. The whole purpose of the row interchange is to make that impossible.

The growth factor is exactly 1/ε − 1

The unpivoted run does not merely produce a large entry — it produces a specific one, and knowing the value rather than a bound makes the claim checkable at every position of the slider.

max|a| is 1 and max|u| is |1 − 1/ε|, so the growth factor is 1/ε − 1 exactly. At ε = 10⁻³ it is 999; at ε = 10⁻¹⁷ it is 10¹⁷. That is asserted as an equality on every drag frame, not as an inequality, which means a change to the code that altered the arithmetic would be caught at every ε rather than at the one that happened to be tested.

The pivoted run’s growth factor is 1. It cannot be otherwise: with the swap, both multipliers are ε, and nothing grows.

The failure is graded, which is worse than a cliff

Drag the slider on the figure and the most useful observation is what does not happen. There is no threshold. At ε = 10⁻² the unpivoted answer is fine. At 10⁻⁶ it has lost a few digits. At 10⁻¹² it has lost most of them. At 10⁻¹⁷ it has lost all of them. The transition is smooth and unmarked.

This is why unpivoted elimination survives in codebases. It works on the test cases, it works on the first few real problems, and then one day the data has a small leading entry and the answer is wrong. Nothing in the output distinguishes that day from the others.

A cliff would be better. A method that failed loudly at a known threshold could be checked against it. What this does instead is degrade at a rate that depends on data that was never inspected, which is the failure mode that costs the most to find.

Growth factor under partial pivoting: the bound, the worst case, and realityGrowth factor against matrix size on a logarithmic vertical axis. The two-to-the-n bound rises as a straight line; Wilkinson's matrix sits exactly on it; random matrices stay near one.0816243240110²10⁴10⁶10⁸10¹⁰10¹²10¹⁴matrix size ngrowth factor max|u| / max|a|the 2ⁿ⁻¹ boundworst of 30 randommedian randomWilkinson's matrix sits on the bound30 Gaussian matrices per sizeat n = 40: bound 5.5·10¹¹, worst 4.8
Fig. 2 Why the swap works in general and not only here. With pivoting, the growth factor of thirty random matrices at each size stays near three — the bound is 2ⁿ⁻¹ and reality is nowhere near it. Without pivoting, growth is unbounded: the same measurement on twelve-by-twelve matrices with no swaps produces values orders of magnitude larger, which is what the essay’s refusal check requires.

Backward error says which, if it is computed

The one thing that does distinguish the good run from the bad one is available and cheap.

The pivoted solve has a backward error of 0. Exactly zero — the computed answer is the exact solution of the system as given. The unpivoted solve has a backward error of 0.25.

That number is worth staring at. It says the answer returned is the exact solution of a system 25% away from the one that was asked about. Not 25% in some obscure component: a quarter of the size of the matrix, in the norm. No amount of ill-conditioning excuses that, because backward error is purely the algorithm’s contribution — the exact answer to a nearby problem is the essay about why that separation is available at all.

And here is the diagnostic value. The matrix in question has κ ≈ 2.6, which is about as well conditioned as a matrix can be. A well-conditioned problem with a huge backward error is unambiguous: the algorithm is at fault, and no amount of precision or reformulation is the answer. Swapping two rows is.

Why “just check for a zero pivot” is not enough

The naive defence is to test whether the pivot is zero and swap only then. It fixes the case where the algorithm cannot proceed and leaves the case where it proceeds badly, which is the one that loses answers.

There is no threshold that repairs it either. Any rule of the form “swap if the pivot is smaller than δ” has to pick δ, and the right value depends on the sizes of the other entries in the column — which is exactly the comparison partial pivoting performs. Choosing the largest available entry is not a heuristic; it is the only scale-invariant rule, and it is the reason partial pivoting is not merely a strategy but the strategy.

Two stronger strategies exist. Complete pivoting searches the whole remaining submatrix rather than one column, which gives a much better growth bound — polynomial rather than exponential — at a cost of O(n³) comparisons, which is the same order as the arithmetic. Nobody uses it, because the growth bound partial pivoting fails to achieve is one it does not need in practice. Rook pivoting sits between them. The fact that the weakest of the three is universal is a small lesson in what bounds are worth.

The refusal

An essay claiming that an algorithm fails owes a demonstration that the check could tell. Three assertions run on every build, and each is capable of firing.

The pivoted solve must be stable. Backward error below 10⁻¹⁵. If a change made pivoting stop working, this fires.

The unpivoted solve must be worse. Its forward error must exceed the pivoted one. If a change made the unpivoted version accidentally work, this fires — and that matters, because an essay whose central claim has quietly become false is worse than no essay.

The growth must be exactly 1/ε − 1. An equality, checked to nine digits, at all seventeen positions of the slider.

There is a fourth in lib/matrix.js, which is the version of this system with a hard zero in the corner: with pivoting, the answer is exact; without, the factorisation breaks down and the routine must report it rather than returning infinities. A solver that returns NaN is a solver whose output a figure will draw as an empty picture, which is the failure this site has been caught by before. Assertions that reject is the thread.

Gaussian elimination on a 4×4, one step at a timeFour copies of the same matrix: as given, and after each of the three elimination steps. The pivot in use is outlined and the entries reduced to zero are greyed.21-13-3-121-212-443-12as givenrows in the order 1 2 3 443-1201.251.252.502.51.5-30-0.5-0.52after step 1pivot 443-1202.51.5-3000.5400-0.21.4after step 2pivot 2.543-1202.51.5-3000.540003after step 3pivot 0.5‖PA − LU‖/‖A‖0largest multiplier0.75row order 4 3 2 1the pivot is chosen
Fig. 3 What the pivoted algorithm does on an ordinary matrix, for comparison. Three swaps, every multiplier below one, and a factorisation residual of 10⁻¹⁷. The row order after pivoting is printed at the bottom, because getting that permutation wrong is the other way to produce confident nonsense.

The scaling that defeats the rule

Partial pivoting chooses the largest entry in the column, and there is a case where that is the wrong entry — worth knowing because it is the one exception, and because the fix is not more pivoting.

Multiply the first row of the two-by-two by 10¹⁸. The system is unchanged mathematically: the same equation, scaled. But now the entry in the corner is 10¹⁸ε rather than ε, and if ε is 10⁻¹⁷ that is 10, which is larger than the 1 below it. Partial pivoting looks at the column, sees 10 against 1, and declines to swap — reproducing exactly the failure it exists to prevent.

The problem is that “largest entry in the column” is only a sensible criterion when the rows are comparably scaled. Scaled partial pivoting fixes it by comparing each candidate against the largest entry in its own row rather than in absolute terms, and it is what a careful implementation does when the rows may have wildly different magnitudes.

The deeper point is that row scaling is a property of how the problem was written down rather than of the problem, and a pivoting rule that depends on it is depending on an arbitrary choice. Equilibrating the matrix first — scaling rows and columns so the entries are comparable — makes the question go away and usually reduces the condition number as well, which is the recommendation in the condition number is an amplifier.

The spacing between consecutive numbers at 53-bit precisionA log–log staircase of the gap between neighbouring representable numbers against magnitude. The gap doubles at every power of two and reaches one whole unit partway along.110³10⁶10⁹10¹²10¹⁵10¹⁸10⁻¹⁸10⁻¹⁵10⁻¹²10⁻⁹10⁻⁶10⁻³110³magnitude of the numbergap to the next representable numbera gap of one whole unitat 1: 2.2·10⁻¹⁶at a million: 1.2·10⁻¹⁰gap reaches 1: 153-bit significandthe gap follows the magnitude
Fig. 4 The mechanism behind the failure, at the level of the arithmetic. The gap between representable numbers at 10¹⁷ is 16, so adding 1 to a number of that size changes nothing. Elimination without the swap produces exactly that situation: a multiplier of 10¹⁷ raises an entry to a magnitude where the information carried by the entries around it is below the gap.

Two more pivoting strategies, and why nobody uses them

Complete pivoting searches the entire remaining submatrix at each step and swaps both a row and a column, bringing the largest available entry to the pivot position. Its growth factor is bounded by roughly n^(¼ log n) — polynomial rather than exponential, and a genuine improvement on partial pivoting’s guarantee.

It costs O(n³) comparisons across the factorisation, which is the same order as the arithmetic, so it is roughly twice as slow. It also destroys any structure the matrix had — a banded matrix does not stay banded — and it requires tracking a column permutation as well as a row one.

Rook pivoting searches alternately along rows and columns until it finds an entry that is largest in both, which usually terminates quickly. Its growth bound is between the other two and its expected cost is close to partial pivoting’s.

Neither is used in general-purpose libraries, and the reason is the subject of the bound that is never attained: partial pivoting’s guarantee is useless and its behaviour is excellent, so paying for a better guarantee buys nothing observable. That is a decision the field made empirically and has never had to revisit.

What a pivot search costs

Worth quantifying, because “pivoting is free” is asserted more often than it is measured.

The search at step k examines n − k entries, so the total across the factorisation is about n²/2 comparisons, against n³/3 multiply-and-subtract pairs for the arithmetic. At n = 100 that is 5,000 comparisons against 333,000 arithmetic operations: about 1.5%, and comparisons are cheaper than multiplications.

The row interchange itself is the more interesting cost. Swapping two rows of a large matrix moves n numbers and, on a machine with a cache, may move them a long way. In a blocked implementation the swaps are deferred and applied to a whole panel at once for exactly this reason, and on distributed memory the interchange can require communication, which is why alternative schemes such as tournament pivoting exist at scale.

For anything that fits in memory the answer is unambiguous: pivoting costs a couple of percent and buys the difference between an answer and a number.

What this generalises to

The pattern here appears three more times on this site and it is worth naming.

An algorithm has two variants. They are algebraically identical — the same derivation produces both, and in exact arithmetic they return the same answer. One of them forms a quantity from operands of wildly different magnitude and the other does not. The second is stable and the first is not, the difference is invisible in the derivation, and it is visible in one measured number.

Two Gram–Schmidts is the same shape: one word changed in a line of pseudocode, and eight orders of magnitude in ‖QᵀQ − I‖. The road that squares the problem is the same shape again, with a whole method rather than a line.

In each case the lesson is not “be careful”. It is that the choice exists, that it has a right answer, and that the right answer costs nothing — one comparison per step here, one variable’s worth of bookkeeping there. The expensive part is knowing the choice was there at all, which is what running the failing version is for.

The same shape, three more times

The pattern of this essay recurs, and recognising it is worth more than any of its instances.

An algorithm has two variants that a derivation cannot tell apart. One of them, at some point, forms a quantity from operands of wildly different magnitude. That variant is not backward stable, the other is, the cost of choosing correctly is nil, and the failure is silent.

Two Gram–Schmidts differs by which vector a projection coefficient is computed against — one word — and produces a Q that is not orthogonal on a matrix where the other produces one that is. The road that squares the problem differs by whether AᵀA is formed, and turns a condition number of 10⁶ into 10¹². Cancellation takes the answer is the same pattern in a single expression, where (1 − cos x)/x² and 2 sin²(x/2)/x² are the same function and differ by sixteen orders of magnitude.

In every case the derivation is silent, the failure is quiet, and the fix is free. That is why this site runs the algorithms rather than describing them: the difference is not visible in the algebra, and it is completely visible in one measured number.

Loss of orthogonality against condition number, in binary64A log–log plot of the norm of Q-transpose-Q minus the identity against condition number. Classical Gram–Schmidt rises steeply, modified Gram–Schmidt rises gently, and Householder is flat.110²10⁴10⁶10⁸10¹⁰10¹²10⁻¹⁷10⁻¹⁴10⁻¹¹10⁻⁸10⁻⁵10⁻²10¹condition number κ(A)‖QᵀQ − I‖classicalmodifiedHouseholderκ²uκu8×8, eight seeds per κ, binary64all three reconstruct A
Fig. 5 The same shape in the orthogonality field. Two algorithms with one word between them, measured across eleven decades of conditioning, and a third that does not degrade at all. Nothing in the derivations distinguishes the first two.

What to do in practice

Use a library. LAPACK’s dgesv pivots, has done for forty years, and is faster than anything hand-written.

Where a hand-written version is unavoidable — and there are reasons to, including the one this site is built on — then the checklist is short. Pivot, always, without a threshold. Record the permutation and apply it to the right-hand side. Print ‖PA − LU‖/‖A‖ once and confirm it is at rounding level. Track the largest multiplier and confirm it is at most one, which is a one-line check that catches a pivot search comparing the wrong quantity.

And when a solve surprises, compute the backward error before doing anything else. It costs a matrix–vector product and it says which half of the problem to look at — a small residual is not a small error is about what that number does and does not establish, which is the other half of the same skill.

The exact solution of a 13×13 Hilbert system beside the computed oneTwo columns of numbers: the exact answer, which is the integers one to thirteen, and the answer double-precision elimination returns, with the number of correct digits beside each.H13 x = b, b formed exactly so that x = (1, 2, …, 13)12345678910111213exact1.00002.00003.00133.97865.18864.993010.46720.048921.2657-2.575319.21478.906013.5113computed6.6 correct digits4.8 correct digits3.4 correct digits2.3 correct digits1.4 correct digit0.8 correct digitno correct digitsno correct digitsno correct digitsno correct digitsno correct digits0.6 correct digit1.4 correct digitbackward error2.2·10⁻¹⁷κ = 1.7·10¹⁸The algorithm solved a neighbouring problem perfectly. That problem's answer is this one.right-hand side built in BigInt rationalsthe truth is known
Fig. 6 The case where the backward error is small and the answer is wrong anyway, for contrast with this essay’s. Here the elimination is blameless and the problem is impossible; in the figure at the top of this page the problem is easy and the elimination is at fault. Telling those two apart is the single most useful thing the framework provides.