Skip to main content

Linear Algebra

Language revisions

C++20
Concepts permit the vector-space axioms to be stated as an interface; ranges supply the traversal vocabulary.
C++23
std::mdspan gives a standard non-owning matrix reference with a layout policy.P0009R18
C++26
std::linalg supplies BLAS operations over mdspan, so generic code can dispatch to vendor kernels.P1673R13

Definition​

Vector spaces​

Bases and dimension​

A basis is a linearly independent spanning set. Every finite-dimensional space has one, all bases have the same cardinality, and that cardinality is the dimension. Computationally the choice of basis is everything: the same map has a diagonal matrix in one basis and a dense ill-conditioned one in another, and the whole subject of numerical linear algebra can be read as the search for bases in which a problem is well behaved.

The subspaces attached to a map​

The two subspaces attached to a linear map are its range ran⁡(T)={Tx:x∈V}\operatorname{ran}(T) = \{Tx : x \in V\} and its kernel ker⁡(T)={x:Tx=0}\ker(T) = \{x : Tx = 0\}, related by the rank-nullity theorem

dim⁡ran⁡(T)+dim⁡ker⁡(T)=dim⁡V.\dim \operatorname{ran}(T) + \dim \ker(T) = \dim V .

The four fundamental subspaces, ran⁡(A)\operatorname{ran}(A), ker⁡(A)\ker(A), ran⁡(A∗)\operatorname{ran}(A^{*}) and ker⁡(A∗)\ker(A^{*}), are what the singular value decomposition exhibits explicitly, and they are the reason that decomposition answers questions about rank, least squares and null spaces simultaneously.

Matrices as representations of maps​

Once bases are fixed, a linear map T:Fn→FmT : F^n \to F^m is represented by a unique matrix A∈Fm×nA \in F^{m \times n} with T(x)=AxT(x) = Ax, and composition of maps corresponds to matrix multiplication.

The distinction between the map and the matrix is not pedantic in computation. An operator may be applicable without any matrix existing in memory, and most large-scale solvers in Scientific Computing exploit exactly that. The entry Conjugate Gradient is built on the distinction, and the concept hierarchy below is designed around it.

Change of basis and similarity​

A change of basis by an invertible PP transforms AA into P−1APP^{-1}AP. Quantities invariant under this transformation, namely the eigenvalues, the trace, the determinant and the characteristic polynomial, are properties of the map. Quantities that are not invariant, notably the entries and the condition number in a fixed norm, are properties of the representation.

Numerical methods that improve accuracy by rescaling are exploiting precisely this difference: equilibration replaces AA by D1AD2D_1 A D_2 with diagonal DiD_i, which changes the condition number without changing the underlying problem.

Inner products and norms​

Cauchy-Schwarz and the geometry it supplies​

The inequality ∣⟨x,y⟩∣≤∥x∥∥y∥|\langle x, y\rangle| \le \lVert x\rVert \lVert y\rVert is what makes angles definable, and it is the source of most estimates in Krylov theory. Its equality case, attained exactly when xx and yy are parallel, is what makes the Rayleigh quotient a variational characterisation of eigenvalues rather than merely a formula.

Vector norms​

∥x∥1=∑i∣xi∣,∥x∥2=(∑i∣xi∣2)1/2,∥x∥∞=max⁡i∣xi∣.\lVert x \rVert_1 = \sum_i |x_i|, \qquad \lVert x \rVert_2 = \Big(\sum_i |x_i|^2\Big)^{1/2}, \qquad \lVert x \rVert_\infty = \max_i |x_i|.

The 22-norm is the one with geometry, the ∞\infty-norm the one with a componentwise reading, and the 11-norm the one that promotes sparsity when used as a penalty. Which appears in a bound is not incidental: a bound in the ∞\infty-norm says every component is accurate, while the same bound in the 22-norm permits one component to absorb all the error.

Matrix norms​

∥A∥1=max⁡j∑i∣aij∣,∥A∥∞=max⁡i∑j∣aij∣,∥A∥2=σ1,∥A∥F=(∑i,j∣aij∣2)1/2.\lVert A \rVert_1 = \max_j \sum_i |a_{ij}|, \quad \lVert A \rVert_\infty = \max_i \sum_j |a_{ij}|, \quad \lVert A \rVert_2 = \sigma_1, \quad \lVert A \rVert_F = \Big( \sum_{i,j} |a_{ij}|^2 \Big)^{1/2}.

Induced norms are submultiplicative, ∥AB∥≤∥A∥∥B∥\lVert AB \rVert \le \lVert A \rVert \lVert B \rVert, and every error analysis in this reference relies on that inequality.

NormCost to computeSubmultiplicativeUnitarily invariant
∥A∥1\lVert A\rVert_1Θ(mn)\Theta(mn)yesno
∥A∥∞\lVert A\rVert_\inftyΘ(mn)\Theta(mn)yesno
∥A∥2\lVert A\rVert_2Θ(mn2)\Theta(mn^2) or estimatedyesyes
∥A∥F\lVert A\rVert_FΘ(mn)\Theta(mn)yesyes

The table explains a practice that otherwise looks like carelessness: analyses are stated in the 22-norm, where the theory is cleanest, and computations use the 11- or ∞\infty-norm, where the quantity is cheap, with the equivalence constants absorbed into the statement.

Equivalence of norms, and why it does not settle the matter​

All norms on a finite-dimensional space are equivalent, so convergence in one implies convergence in all. The equivalence constants, however, depend on the dimension:

∥x∥2≤∥x∥1≤n ∥x∥2,∥x∥∞≤∥x∥2≤n ∥x∥∞.\lVert x\rVert_2 \le \lVert x \rVert_1 \le \sqrt{n}\,\lVert x\rVert_2, \qquad \lVert x\rVert_\infty \le \lVert x \rVert_2 \le \sqrt{n}\,\lVert x\rVert_\infty .

At n=106n = 10^6 a factor of n=103\sqrt{n} = 10^3 is three significant digits, so a tolerance stated in the wrong norm is a tolerance wrong by three digits. Norms are therefore stated explicitly in every bound in this reference.

Orthogonality​

A set is orthonormal when ⟨qi,qj⟩=δij\langle q_i, q_j \rangle = \delta_{ij}. Orthonormal bases are the numerically preferred representation of a subspace because the associated matrix QQ satisfies ∥Qx∥2=∥x∥2\lVert Qx \rVert_2 = \lVert x \rVert_2, so applying QQ neither amplifies nor damps error.

Projections​

The orthogonal projection onto the range of an orthonormal QQ is P=QQ∗P = QQ^{*}, and it satisfies P2=PP^2 = P, P∗=PP^{*} = P, ∥P∥2=1\lVert P \rVert_2 = 1. The last equality is what distinguishes an orthogonal projection from an oblique one, whose norm can be arbitrarily large and which therefore amplifies error in proportion.

Least squares is the projection statement: the minimiser of ∥Ax−b∥2\lVert Ax - b\rVert_2 is the xx for which AxAx is the orthogonal projection of bb onto ran⁡(A)\operatorname{ran}(A), hence the normal equations A∗Ax=A∗bA^{*}Ax = A^{*}b, which are the right statement and the wrong algorithm.

Householder reflections and Givens rotations​

Two families of orthogonal transformations do all the work:

H=I−2vv∗v∗v(reflection),G(i,j,θ)(rotation in a coordinate plane).H = I - 2\frac{vv^{*}}{v^{*}v} \quad \text{(reflection)}, \qquad G(i,j,\theta) \quad \text{(rotation in a coordinate plane)}.

A Householder reflection zeroes an entire column below the diagonal in one application at cost Θ(mn)\Theta(mn) for the trailing update; a Givens rotation zeroes one entry and is used where the matrix is structured, since it disturbs only two rows. Both preserve the 22-norm exactly to working accuracy, which is why they replace the algebraically simpler elimination steps.

Structured matrices​

Structure is what makes large problems tractable, and each kind carries its own cost model.

StructureStorageSolve costWhere it arises
DenseΘ(n2)\Theta(n^2)Θ(n3)\Theta(n^3)Small problems, dense blocks
TriangularΘ(n2/2)\Theta(n^2/2)Θ(n2)\Theta(n^2)Factors of a decomposition
Banded, bandwidth bbΘ(nb)\Theta(nb)Θ(nb2)\Theta(nb^2)One-dimensional discretisations
Sparse, nnz\mathrm{nnz} entriesΘ(nnz)\Theta(\mathrm{nnz})Fill-dependentDiscretised PDEs, graphs
Toeplitz or circulantΘ(n)\Theta(n)Θ(nlog⁡n)\Theta(n\log n)Convolution, stationary processes
Low rank, rank kkΘ((m+n)k)\Theta((m+n)k)Θ((m+n)k)\Theta((m+n)k)Compression, kernel methods

Recognising structure is worth more than any implementation effort applied to the dense form: the difference between Θ(n3)\Theta(n^3) and Θ(nlog⁡n)\Theta(n \log n) at n=106n = 10^6 is not a factor that tuning can recover.

Motivation​

Why linear algebra is the computational core​

Most large computations are either linear or are solved by a sequence of linear problems. Newton's method solves Jδ=−rJ\delta = -r at each step. An implicit time integrator solves a linear system per step. A discretised elliptic partial differential equation is a single large sparse system. Least squares, eigenvalue problems and low-rank approximation are the standard tools of data analysis.

Why it is the performance core​

Matrix multiplication has arithmetic intensity growing with the blocking factor, which makes it the one dense kernel able to reach a large fraction of peak floating-point throughput. Algorithms are therefore reformulated to spend their time in matrix-matrix products, that is, in blocked factorizations rather than unblocked ones, and the reformulation is a linear-algebraic exercise before it is a performance one. The quantitative version of the argument is in Matrix Multiplication.

The three levels​

The BLAS classification is a statement about arithmetic intensity, not about interfaces:

LevelOperationDataFlopsIntensity
1y←αx+yy \leftarrow \alpha x + yΘ(n)\Theta(n)Θ(n)\Theta(n)Θ(1)\Theta(1)
2y←αAx+βyy \leftarrow \alpha Ax + \beta yΘ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)Θ(1)\Theta(1)
3C←αAB+βCC \leftarrow \alpha AB + \beta CΘ(n2)\Theta(n^2)Θ(n3)\Theta(n^3)Θ(n)\Theta(n)

Only level 3 has intensity growing with the problem size, and only level 3 can be compute-bound. Every performance decision in dense linear algebra follows from this table.

Two standing errors​

Forming the inverse​

Solving Ax=bAx = b by computing A−1A^{-1} costs three times an LU factorization, is less accurate, and destroys any structure AA possessed. The appearance of A−1A^{-1} in a formula is an instruction to solve a system, and it is read that way throughout this reference. The only case in which an explicit inverse is wanted is when its entries are themselves the answer, which is rare.

Forming the normal equations​

The normal equations A∗Ax=A∗bA^{*}Ax = A^{*}b are the correct characterisation of the least-squares solution and a poor algorithm, because κ2(A∗A)=κ2(A)2\kappa_2(A^{*}A) = \kappa_2(A)^2: half the available digits are lost before the solve begins. QR factorization costs about twice as much and does not square the conditioning.

Formal formulation​

The decompositions and what each reveals​

DecompositionFormRevealsCost
LUPA=LUPA = LUSolutions, determinant23n3\tfrac{2}{3}n^3
CholeskyA=LL∗A = LL^{*}Positive definiteness13n3\tfrac{1}{3}n^3
QRA=QRA = QRRange, least squares2mn2−23n32mn^2 - \tfrac{2}{3}n^3
EigenA=XΛX−1A = X\Lambda X^{-1}Dynamics, powersΘ(n3)\Theta(n^3), iterative
SchurA=QTQ∗A = QTQ^{*}Eigenvalues, stablyΘ(n3)\Theta(n^3), iterative
SVDA=UΣV∗A = U\Sigma V^{*}Rank, all four subspaces, best approximationΘ(mn2)\Theta(mn^2)

The ordering is roughly by increasing cost and increasing information. A method is chosen by the least informative decomposition that answers the question, which is why a solve uses LU and not the SVD despite the latter answering strictly more.

Spectra​

The spectrum is σ(A)={λ:det⁡(A−λI)=0}\sigma(A) = \{\lambda : \det(A - \lambda I) = 0\}, with spectral radius ρ(A)=max⁡λ∈σ(A)∣λ∣\rho(A) = \max_{\lambda \in \sigma(A)} |\lambda|.

Convergence of stationary iterations​

The condition is on the spectral radius and not on any norm, which matters because ρ(M)≤∥M∥\rho(M) \le \lVert M \rVert for every induced norm, with strict inequality possible. An iteration can therefore converge while ∥M∥>1\lVert M\rVert > 1, and it will do so after an initial phase of growth whose length is governed by the departure from normality.

The spectral theorem​

For a symmetric or Hermitian matrix, A=QΛQ∗A = Q\Lambda Q^{*} with QQ unitary and Λ\Lambda real diagonal. Three consequences are used constantly: the eigenvalues are real and well conditioned; the eigenvectors are orthogonal, so the change of basis costs nothing in accuracy; and the variational characterisation

λmax⁡=max⁡x≠0x∗Axx∗x,λmin⁡=min⁡x≠0x∗Axx∗x\lambda_{\max} = \max_{x \neq 0} \frac{x^{*}Ax}{x^{*}x}, \qquad \lambda_{\min} = \min_{x \neq 0} \frac{x^{*}Ax}{x^{*}x}

makes eigenvalues accessible to optimisation methods.

Non-normal matrices and pseudospectra​

A matrix is normal when AA∗=A∗AAA^{*} = A^{*}A, equivalently when it is unitarily diagonalisable. For non-normal matrices the eigenvalues can be badly conditioned and can mispredict transient behaviour: ∥Ak∥\lVert A^k \rVert may grow by many orders of magnitude before decaying, even when ρ(A)<1\rho(A) < 1.

The ε\varepsilon-pseudospectrum, σε(A)={z:∥(zI−A)−1∥2≥ε−1}\sigma_\varepsilon(A) = \{z : \lVert (zI - A)^{-1}\rVert_2 \ge \varepsilon^{-1}\}, is the appropriate substitute, and it is why stability analyses of discretised advection problems cannot be settled by eigenvalues alone.

Singular values​

The singular values σ1≥⋯≥σn≥0\sigma_1 \ge \cdots \ge \sigma_n \ge 0 of AA are the square roots of the eigenvalues of A∗AA^{*}A, and A=UΣV∗A = U\Sigma V^{*} is the singular value decomposition.

Best approximation​

This theorem justifies every low-rank method: it says that the singular value spectrum, not the sparsity pattern or the size, determines how far a matrix can be compressed. Its randomised counterpart is in Probability.

Rank in floating point​

Exact rank is not a computable property of a floating-point matrix: arbitrarily small perturbations change it. The usable substitute is the numerical rank at a tolerance,

rank⁡ε(A)=#{i:σi>εσ1},\operatorname{rank}_\varepsilon(A) = \#\{ i : \sigma_i > \varepsilon \sigma_1 \},

which is stable under perturbations smaller than εσ1\varepsilon\sigma_1 and is what every rank-revealing algorithm actually returns. A routine that reports an integer rank without a tolerance is reporting an artefact of its own arithmetic.

Conditioning​

The rule of thumb​

With κ2(A)≈10k\kappa_2(A) \approx 10^{k} in binary64, roughly 16−k16 - k significant decimal digits survive. At κ2=1016\kappa_2 = 10^{16} nothing survives, and the computed solution carries no information about the exact one, whatever algorithm produced it.

κ2(A)\kappa_2(A)Digits surviving in binary64Practical reading
10210^{2}14Well conditioned
10610^{6}10Ordinary
101010^{10}6Attention required
101410^{14}2Reformulate or use higher precision
101610^{16}0Numerically singular

Componentwise conditioning​

The norm-wise condition number can be pessimistic for badly scaled problems, where one row is tiny. The componentwise condition number, cond(A,x)=∥∣A−1∣∣A∣∣x∣∥/∥x∥\mathrm{cond}(A, x) = \lVert |A^{-1}||A||x| \rVert / \lVert x \rVert, is invariant under row scaling and is the right quantity when the data have different units. It explains why equilibration helps: it does not change the componentwise conditioning, but it makes the norm-wise estimate reflect it.

Positive definiteness​

AA is symmetric positive definite when A=A∗A = A^{*} and x∗Ax>0x^{*}Ax > 0 for all x≠0x \neq 0.

Equivalent characterisations​

All eigenvalues positive; all leading principal minors positive; a Cholesky factorization exists with positive diagonal; A=B∗BA = B^{*}B for some BB of full column rank. The third is the only one cheap to test, and it is a test by attempted computation rather than by inspection, which is why a Cholesky factorization that fails is informative rather than merely unsuccessful.

The A-inner product​

Positive definiteness makes ⟨x,y⟩A=x∗Ay\langle x, y\rangle_A = x^{*}Ay an inner product, with induced norm ∥x∥A\lVert x\rVert_A. Conjugate gradient minimises the error in exactly this norm, which is why its convergence bound is stated in ∥⋅∥A\lVert \cdot \rVert_A and why a bound in the 22-norm requires an extra factor of κ\sqrt{\kappa}.

It also removes the need for pivoting in Cholesky factorization, which halves the arithmetic and removes data-dependent control flow from the inner loop.

Krylov subspaces​

Krylov methods are the algorithms available when the only operation is x↦Axx \mapsto Ax. Their convergence is governed by polynomial approximation on the spectrum: after kk steps the error is

∥xk−x∗∥A≤min⁡p∈Pk,  p(0)=1  max⁡λ∈σ(A)∣p(λ)∣  ∥x0−x∗∥A,\lVert x_k - x^{*}\rVert_A \le \min_{p \in \mathcal{P}_k,\; p(0)=1} \; \max_{\lambda \in \sigma(A)} |p(\lambda)| \; \lVert x_0 - x^{*}\rVert_A,

which is why a spectrum clustered away from the origin converges quickly regardless of κ\kappa, and why preconditioning is best understood as reshaping the spectrum rather than as reducing a single number.

C++ representation​

Spaces and operators as concepts​

#include <concepts>
#include <cstddef>

namespace hpc {

template <class V, class F>
concept VectorSpace =
std::regular<V> &&
Field<F> &&
requires(V x, V y, F a) {
{ x + y } -> std::same_as<V>;
{ x - y } -> std::same_as<V>;
{ a * x } -> std::same_as<V>;
{ V{} } -> std::same_as<V>;
};

template <class V, class F>
concept InnerProductSpace =
VectorSpace<V, F> &&
requires(const V& x, const V& y) {
{ inner_product(x, y) } -> std::same_as<F>;
{ norm(x) } -> std::convertible_to<F>;
};

// A linear operator need not be a matrix: only its action is required.
template <class A, class V>
concept LinearOperator = requires(const A& op, const V& x, V& y) {
{ op.rows() } -> std::convertible_to<std::size_t>;
{ op.cols() } -> std::convertible_to<std::size_t>;
op.apply(x, y); // y <- A x
};

template <class A, class V>
concept SelfAdjointOperator = LinearOperator<A, V>;
// Semantic requirement: <Ax, y> == <x, Ay> for all x, y.

// Some methods need the adjoint action as well; requiring it separately keeps
// matrix-free operators that cannot supply it usable by the rest.
template <class A, class V>
concept OperatorWithAdjoint = LinearOperator<A, V> &&
requires(const A& op, const V& x, V& y) {
op.apply_adjoint(x, y); // y <- A* x
};

} // namespace hpc

Why the operator concept is the load-bearing one​

LinearOperator admits a dense matrix, a sparse matrix, a finite-difference stencil applied without assembly, a factored preconditioner, and a Gram operator B∗BB^{*}B applied as two products without forming the product. The last is the case that justifies the abstraction: forming B∗BB^{*}B costs Θ(mn2)\Theta(mn^2) and squares the condition number, whereas applying it costs Θ(mn)\Theta(mn) and does neither.

Splitting the adjoint requirement​

Separating apply_adjoint into its own concept is a deliberate refinement rather than a single larger interface. Symmetric operators supply it for free, matrix-free stencils often cannot supply it at all, and methods differ in whether they need it: conjugate gradient does not, LSQR and GMRES-based least squares do. Splitting the requirement means the operators that cannot supply an adjoint remain usable by the methods that do not need one.

Semantic obligations​

// Semantic requirements for VectorSpace<V, F>:
// S1. (V, +) is an abelian group.
// S2. Scalar multiplication distributes over both additions.
// S3. 1 * x == x.
//
// For InnerProductSpace, additionally:
// S4. <x, y> is linear in its second argument and conjugate-symmetric.
// S5. <x, x> > 0 for x != 0.
// S6. norm(x) == sqrt(<x, x>).
//
// For LinearOperator, additionally:
// S7. apply is linear: A(ax + y) == a A x + A y.
// S8. apply is a pure function of its input.
//
// For SelfAdjointOperator, additionally:
// S9. <Ax, y> == <x, Ay>.

S1 to S3 fail for floating-point vectors by the bounds of Numerical Analysis. S7 is testable cheaply and probabilistically, and S9 is the obligation conjugate gradient depends on that no compiler checks, which is why the implementation there tests the curvature it computes rather than trusting the caller.

Dimensions as types or as values​

Fixed extents make conformability a deduction problem; dynamic extents make it a precondition:

#include <cstddef>
#include <mdspan>

namespace hpc {

// Static extents: a non-conforming product fails to deduce.
template <class T, std::size_t M, std::size_t K, std::size_t N>
constexpr auto multiply(std::mdspan<const T, std::extents<std::size_t, M, K>> A,
std::mdspan<const T, std::extents<std::size_t, K, N>> B)
-> std::mdspan<T, std::extents<std::size_t, M, N>>;

// Dynamic extents: conformability is a documented precondition.
// Requires: A.extent(1) == B.extent(0).
void multiply(std::mdspan<const double, std::dextents<std::size_t, 2>> A,
std::mdspan<const double, std::dextents<std::size_t, 2>> B,
std::mdspan<double, std::dextents<std::size_t, 2>> C) noexcept;

} // namespace hpc

The static form is right for small fixed shapes, where it also unlocks full unrolling; the dynamic form is right above a few tens, where an instantiation per shape would dominate build time and instruction cache. The rule and its justification are in Type-Level Programming.

Storage and layout as policy​

A matrix type conflates three decisions that are better kept separate: the element type, the index mapping, and the ownership of the storage. mdspan separates all three, so a kernel written against it applies to row-major, column-major, strided and tiled layouts without change, and to owned or borrowed memory alike.

The consequence for interfaces is that kernels take mdspan and never containers. A kernel parameterised on a container instantiates once per caller, forces ownership decisions on the caller, and cannot be called on a sub-block; the same kernel on mdspan has none of those properties. The layout question itself is developed in Data Layout.

Implementation​

Norms computed without overflow​

The obvious implementation of ∥x∥2\lVert x\rVert_2 overflows for entries above about 1015410^{154} in binary64 and underflows to zero below about 10−16210^{-162}, in both cases for vectors whose norm is perfectly representable. Production routines scale:

#include <cmath>
#include <span>

namespace hpc {

// Two-pass scaled norm: exact to working accuracy and free of spurious
// overflow. The single-pass sum of squares is faster and wrong at the extremes.
inline double euclidean_norm(std::span<const double> x) noexcept {
double scale = 0.0;
double ssq = 1.0;
for (const double v : x) {
if (v != 0.0) {
const double a = std::abs(v);
if (scale < a) {
const double r = scale / a;
ssq = 1.0 + ssq * r * r;
scale = a;
} else {
const double r = a / scale;
ssq += r * r;
}
}
}
return scale * std::sqrt(ssq);
}

} // namespace hpc

The routine is a standard illustration of the gap between an algebraic definition and a numerical one: the formula is a sum of squares, and the implementation is not.

Conjugate gradient, written against the operator​

#include <cmath>
#include <cstddef>
#include <span>

namespace hpc {

struct cg_result {
std::size_t iterations;
double residual_norm;
};

// Solves A x = b for symmetric positive-definite A. The operator is used only
// through apply(); no representation of A is assumed.
template <class Operator>
cg_result conjugate_gradient(const Operator& A,
std::span<const double> b,
std::span<double> x,
std::span<double> r,
std::span<double> p,
std::span<double> Ap,
double tolerance,
std::size_t max_iterations)
{
A.apply(x, Ap);
for (std::size_t i = 0; i < b.size(); ++i) {
r[i] = b[i] - Ap[i];
p[i] = r[i];
}

double rr = inner_product(r, r);
const double stop = tolerance * tolerance * rr;

for (std::size_t k = 0; k < max_iterations; ++k) {
if (rr <= stop) { return {k, std::sqrt(rr)}; }

A.apply(p, Ap);
const double pAp = inner_product(p, Ap);
if (!(pAp > 0.0)) { return {k, std::sqrt(rr)}; } // S9 violated

const double alpha = rr / pAp;
for (std::size_t i = 0; i < b.size(); ++i) {
x[i] += alpha * p[i];
r[i] -= alpha * Ap[i];
}

const double rr_next = inner_product(r, r);
const double beta = rr_next / rr;
rr = rr_next;

for (std::size_t i = 0; i < b.size(); ++i) {
p[i] = r[i] + beta * p[i];
}
}
return {max_iterations, std::sqrt(rr)};
}

} // namespace hpc

The workspace is supplied by the caller rather than allocated, which keeps the routine usable from a context with a fixed memory budget and makes the Θ(n)\Theta(n) working set explicit in the signature.

Orthogonalisation, and why the obvious form is wrong​

Classical Gram-Schmidt is algebraically correct and numerically useless: the computed vectors lose orthogonality at a rate proportional to κ2(A)2\kappa_2(A)^2. The modified form loses it at a rate proportional to u κ2(A)u\,\kappa_2(A), and Householder reflections do not lose it to first order.

#include <cmath>
#include <cstddef>
#include <span>

namespace hpc {

// Modified Gram-Schmidt: each new vector is orthogonalised against the
// already-orthogonalised basis one vector at a time, using the updated vector
// for each projection.
inline void modified_gram_schmidt(std::span<std::span<double>> columns) {
for (std::size_t j = 0; j < columns.size(); ++j) {
for (std::size_t i = 0; i < j; ++i) {
const double proj = inner_product(columns[i], columns[j]);
for (std::size_t k = 0; k < columns[j].size(); ++k) {
columns[j][k] -= proj * columns[i][k];
}
}
const double nrm = std::sqrt(inner_product(columns[j], columns[j]));
for (double& v : columns[j]) { v /= nrm; }
}
}

} // namespace hpc

The difference between the two variants is which vector is used to compute the projection, and it changes the attainable accuracy by a factor of κ\kappa. This is the standard illustration that algebraic equivalence does not imply numerical equivalence.

Where full accuracy is required and Householder is unavailable, reorthogonalising once recovers it: two passes of classical Gram-Schmidt give orthogonality to working accuracy, which is the result usually summarised as "twice is enough".

Estimating a condition number​

namespace hpc {

// Hager and Higham 1-norm estimator: about five triangular solves against an
// existing LU factorization, giving a lower bound on ||A^{-1}||_1 that is
// within a small factor in practice.
double estimate_condition_1(const auto& lu_factorization);

} // namespace hpc

An estimate that is occasionally too small by a factor of two is adequate, because the quantity is used to decide how many digits to trust, and that decision is made on an order of magnitude.

Matrix-free operators​

#include <cstddef>
#include <span>

namespace hpc {

// The Gram operator B*B, applied without forming the product. Satisfies
// SelfAdjointOperator, costs Theta(mn) per application against Theta(mn^2) to
// form, and avoids squaring the condition number of the representation.
template <class Operator>
class gram_operator {
public:
explicit gram_operator(const Operator& B, std::span<double> scratch) noexcept
: B_{B}, scratch_{scratch} {}

std::size_t rows() const noexcept { return B_.cols(); }
std::size_t cols() const noexcept { return B_.cols(); }

void apply(std::span<const double> x, std::span<double> y) const {
B_.apply(x, scratch_); // scratch <- B x
B_.apply_adjoint(scratch_, y); // y <- B* (B x)
}

private:
const Operator& B_;
std::span<double> scratch_;
};

} // namespace hpc

This composition is the reason the concept is stated in terms of an action: gram_operator is a model of SelfAdjointOperator built from another operator, and every Krylov method accepts it without modification.

Complexity​

Complexity; n-dimensional problems, arithmetic model
OperationTimeSpaceRemarks
Inner product, denseΘ(n)Θ(1)2n flops, 16n bytes; intensity 1/8
axpyΘ(n)Θ(1)2n flops, 24n bytes; intensity 1/12
Scaled 2-normΘ(n)Θ(1)Branchy; slower than the naive sum of squares
y = αAx + y, denseΘ(mn)Θ(1)Intensity about 1/4; bandwidth-bound
C = AB, dense n × nΘ(n³)Θ(1)Intensity Θ(b) when blocked; compute-bound
Cholesky(1/3)n³ flopsΘ(1) extraNo pivoting needed
LU with partial pivoting(2/3)n³ flopsΘ(n) pivotsBackward stable in practice
Householder QR, m × n2mn² − (2/3)n³Θ(n)Orthogonality to working accuracy
Modified Gram-Schmidt2mn² flopsΘ(1)Orthogonality loss O(u κ)
Classical Gram-Schmidt2mn² flopsΘ(1)Orthogonality loss O(u κ²)
Symmetric eigenproblem(4/3)n³ + Θ(n²)Θ(n²)Tridiagonal reduction dominates
Thin SVD, m ≫ nabout 6mn² + 20n³Θ(mn)Golub and Kahan
Triangular solve, one RHSn² flopsΘ(1)Level 2; bandwidth-bound
Banded solve, bandwidth bΘ(nb²)Θ(nb)Linear in n
CG iterationΘ(nnz + n)Θ(n)One operator application per step
CG to relative tolerance εΘ(√κ log(1/ε)) iterationsΘ(n)κ in the 2-norm

Performance considerations​

Arithmetic intensity organises everything​

The distinction that organises dense linear algebra is operations per byte moved. Level-1 and level-2 operations have intensity O(1)O(1) and are bandwidth-bound; level-3 has intensity Θ(b)\Theta(b) for a blocking factor bb and is the only level that can approach peak.

The consequence is architectural rather than local. An algorithm is restructured so that its arithmetic occurs in level-3 operations: blocked LU performs the same 23n3\tfrac{2}{3}n^3 operations as the unblocked form and runs an order of magnitude faster, because those operations occur inside matrix-matrix products on cache-resident blocks. The transfer-count analysis is in Cache Locality.

Iterative methods invert the picture​

For sparse iterative methods the operator application is bandwidth-bound and irreducibly so. The available levers are the storage format, the ordering of the unknowns, and the preconditioner, which trades operator applications for a better spectrum and is therefore an algorithmic rather than a hardware optimisation. A preconditioner that halves the iteration count and triples the cost per iteration is a loss, and only the product distinguishes them.

Conditioning bounds what any implementation can deliver​

If κ2(A)≈1012\kappa_2(A) \approx 10^{12} in binary64, roughly four significant digits survive, and no arrangement of the arithmetic recovers them. The responses are reformulation, scaling, or extended precision, not tuning. Iterative refinement in a higher precision is the cheapest of these and is developed in Numerical Linear Algebra.

Symmetry is worth exploiting twice​

Exploiting symmetry halves both the storage and the arithmetic, and it also removes pivoting from the factorization, which removes data-dependent control flow from the inner loop. The combination is usually worth more than the factor of two that the operation count alone suggests.

Orthogonalisation is a bandwidth problem​

The inner loop of Gram-Schmidt is a sequence of inner products and updates, all level-1, so an orthogonalisation of nn vectors of length mm moves Θ(mn2)\Theta(mn^2) words for Θ(mn2)\Theta(mn^2) flops. Block variants, which orthogonalise several vectors against the basis at once, convert the level-1 operations into level-3 ones and are worth a large factor at the cost of a more delicate stability argument.

Limitations​

Concepts state the operations of a vector space and cannot state its axioms. VectorSpace<V, F> is satisfied by types whose addition is not associative, which includes every floating-point vector type in use.

The LinearOperator concept cannot state that an operator is symmetric or positive definite. Conjugate gradient applied to an operator that is neither may converge to something, diverge, or stall, without any diagnostic. Where the property is essential it is checked probabilistically at debug level and documented as a precondition.

Dimensions are runtime values in the dynamic interface, so conformability is a precondition rather than a type error. Fixed-extent mdspan moves the check to translation time at the cost of an instantiation per shape.

Rank is not computable in floating point, only numerical rank at a tolerance. Any interface that returns an integer rank without accepting a tolerance is hiding a decision it made on the caller's behalf.

The 22-norm and the condition number in it are not computable at the cost of the operations they qualify, so they are estimated. Estimates are lower bounds, and an estimator that under-reports by a factor of two is normal.

Algebra supplies the field; Numerical Analysis the error model; Numerical Linear Algebra the factorizations; Sparse Computation the structured case; Data Layout the storage decisions; Conjugate Gradient a complete derivation using this material.

References​

  • G. H. Golub and C. F. Van Loan, Matrix Computations, 4th ed., Johns Hopkins, 2013.
  • L. N. Trefethen and D. Bau, Numerical Linear Algebra, SIAM, 1997.
  • N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed., SIAM, 2002.
  • L. N. Trefethen and M. Embree, Spectra and Pseudospectra, Princeton University Press, 2005.
  • C. Eckart and G. Young, The approximation of one matrix by another of lower rank, Psychometrika 1, 1936.
  • N. J. Higham, FORTRAN Codes for Estimating the One-Norm of a Real or Complex Matrix, ACM TOMS 14(4), 1988.
  • L. Giraud, J. Langou and M. Rozloznik, The loss of orthogonality in the Gram-Schmidt orthogonalization process, Computers and Mathematics with Applications 50, 2005.