BLAS, LAPACK and Numerical Libraries
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Fortran 2023 (ISO/IEC 1539-1:2023, J3/23-007r1) and was checked on 2026-10-19. The Fortran
examples were compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran 16.2)
and run, and the examples that call a library were also built and run with LLVM Flang 23; the output shown is real
output. Numerical libraries are not part of the language: they are separate binaries that a Fortran program calls through
procedure interfaces, so the standard says almost nothing about them. What it does say matters, and is cited where it
applies: explicit interfaces in 15.4.2.2, assumed-size dummy arguments in 8.5.8.5, sequence association in 15.5.2.12, and
the matmul and dot_product intrinsics in 16.9.134 and 16.9.73. The libraries are described by their own
documentation: the reference BLAS and LAPACK at netlib, and OpenBLAS, Intel oneMKL, FFTW, HDF5, NetCDF and PETSc by their
projects.
Versions in this page, all checked on 2026-10-19. Reference LAPACK 3.12.1 (released 2025-01-08, the latest release at netlib and
on GitHub), the one in the Debian container used for the examples. OpenBLAS 0.3.29 in the container; the latest release on
GitHub is 0.3.34 (2026-07-16). stdlib 0.8.1. FFTW 3.3.10. The examples are numerical code of the running example,
heatflow: an implicit (backward Euler) step of the heat equation on a rod gives a tridiagonal, symmetric, positive definite
system, which is what the dgesv, dpotrf and dptsv examples solve. How the modules of heatflow fit together is in
Modules and Submodules; array arguments and the meaning of
assumed-size dummies in
Array Arguments: Assumed Shape, Size and Rank;
and the stdlib package itself in Fortran stdlib.
The short version. Fortran has matmul, dot_product, transpose and elemental array arithmetic built in, and for small
problems or a first version that is all a program needs. For anything that is large, repeated or numerically delicate
(solving linear systems, least squares, eigenvalues, factorizations, FFTs), call a library that is tuned for the machine:
BLAS for the building blocks, LAPACK for the algorithms built on them, and stdlib_linalg if a typed, high-level interface
is preferred to writing interfaces by hand.
The layers: BLAS, LAPACK and their implementations
BLAS (Basic Linear Algebra Subprograms) is a specification of low-level routines, originally written in Fortran 77, in three levels. LAPACK (Linear Algebra PACKage) is a library of higher-level algorithms written in terms of BLAS calls, so a fast BLAS makes all of LAPACK fast. Both are interfaces: the netlib distributions are the reference implementations, written for clarity and not for speed, and every serious platform supplies its own optimized implementation of the same interface. A program written against the interface can therefore be moved from the reference library to OpenBLAS, oneMKL or a vendor library by relinking, with no change to the source.
The three BLAS levels differ in what they operate on and, as a consequence, in how much arithmetic they do for each number read from memory. That ratio is what an optimized library exploits.
| Level | Operates on | Work for size n | Examples (double precision names) |
|---|---|---|---|
1 |
vectors |
O(n), about as many operations as values read |
|
2 |
matrix and vector |
O(n^2), memory bound |
|
3 |
matrix and matrix |
O(n^3) operations on O(n^2) data, so blocking can reuse every value many times |
|
Level 3 is the one that matters for performance: an optimized dgemm reaches a large fraction of the peak speed of the
processor, and LAPACK is organised so that its blocked algorithms spend most of their time in level 3 calls.
The naming scheme
BLAS names are x followed by the operation, and the prefix gives the type: s real, d double precision, c complex and
z double complex (dgemm, zgemm). LAPACK names have the form XYYZZZ, as the LAPACK Users' Guide describes them (the sixth letter is
blank for some drivers):
-
the first letter is the data type, as for BLAS:
s,d,corz; -
the next two letters are the matrix type;
-
the last three letters are the computation.
| Matrix type | Meaning | Computation | Meaning |
|---|---|---|---|
|
general (unsymmetric, possibly rectangular) |
|
simple driver: solve a linear system in one call |
|
symmetric |
|
factorize (triangular factorization) |
|
symmetric or Hermitian positive definite |
|
solve using the factorization from |
|
general tridiagonal, positive definite tridiagonal |
|
eigenvalues and eigenvectors |
|
general band, positive definite band |
|
singular value decomposition |
|
triangular |
|
QR factorization |
|
orthogonal (real), unitary (complex) |
|
least squares |
So dgesv reads as d (double precision), ge (general matrix), sv (solve); dpotrf is double, positive definite, triangular
factorization (Cholesky); dpotrs solves with that factor; dsyev is eigenvalues and vectors of a real symmetric matrix;
dptsv solves a symmetric positive definite tridiagonal system. A driver routine (sv, ev, ls) does the whole job;
the computational routines (trf, trs) are the pieces, and are what to call when one factorization is reused for many
right-hand sides. The reference documentation of every routine, with the exact meaning of each argument, is the generated
reference at netlib; read it before the first call of a routine, because the argument conventions are not guessable.
Calling the libraries from Fortran
Column-major storage and the leading dimension
BLAS and LAPACK are Fortran libraries, so a matrix is stored in column-major order, which is exactly how a Fortran array is stored (see Arrays Fundamentals). A Fortran program can pass its arrays straight through with no copying or transposing; a C program using the same libraries must either transpose or use the CBLAS and LAPACKE wrappers, which take an order argument.
Matrix arguments come with a leading dimension: for a(lda, *) the number lda is the declared size of the first
dimension of the array in the caller, the distance between the starts of consecutive columns. It is separate from the
number of rows m of the matrix being used, so that a routine can work on a sub-block of a larger array. The routine
requires lda >= max(1, m) and reports a violation as an illegal argument (see Info codes and error reporting).
Writing explicit interfaces
The library routines are external procedures: ordinary Fortran 77 procedures compiled elsewhere. A call to an external
procedure without an explicit interface is allowed by the standard (15.4.2.2 lists when an explicit interface is
required, and none of them applies to a plain dgemm call), but then the compiler knows nothing about the arguments and checks nothing. The
right habit is to write an explicit interface for every library routine in one module and use it, so that the compiler
checks the type, kind, rank and intent of every call. The module below does that for four BLAS routines. Dummy arrays are
assumed-size (8.5.8.5): x(*) and a(lda, *) say "an array of unknown size", which is how an F77 array argument looks
to Fortran 2023, and they are what lets the caller pass any array, array element or section of the right kind. import
gives the interface body access to the kind dp, because an interface body does not see its host.
module blas_interfaces
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: ddot, daxpy, dgemv, dgemm
interface
function ddot(n, x, incx, y, incy) result(r)
import :: dp
integer, intent(in) :: n, incx, incy
real(dp), intent(in) :: x(*), y(*)
real(dp) :: r
end function ddot
subroutine daxpy(n, alpha, x, incx, y, incy)
import :: dp
integer, intent(in) :: n, incx, incy
real(dp), intent(in) :: alpha, x(*)
real(dp), intent(inout) :: y(*)
end subroutine daxpy
subroutine dgemv(trans, m, n, alpha, a, lda, x, incx, beta, y, incy)
import :: dp
character, intent(in) :: trans
integer, intent(in) :: m, n, lda, incx, incy
real(dp), intent(in) :: alpha, beta, a(lda, *), x(*)
real(dp), intent(inout) :: y(*)
end subroutine dgemv
subroutine dgemm(transa, transb, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc)
import :: dp
character, intent(in) :: transa, transb
integer, intent(in) :: m, n, k, lda, ldb, ldc
real(dp), intent(in) :: alpha, beta, a(lda, *), b(ldb, *)
real(dp), intent(inout) :: c(ldc, *)
end subroutine dgemm
end interface
end module blas_interfaces
program blas_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use blas_interfaces, only: ddot, daxpy, dgemv, dgemm
implicit none
real(dp) :: x(3), y(3), a(2, 3), b(3, 2), c(2, 2), v(2)
x = [1.0_dp, 2.0_dp, 3.0_dp]
y = [4.0_dp, 5.0_dp, 6.0_dp]
print '(a, f6.1, a, f6.1)', 'ddot =', ddot(3, x, 1, y, 1), ' dot_product =', dot_product(x, y)
call daxpy(3, 2.0_dp, x, 1, y, 1) ! y := 2*x + y
print '(a, 3f6.1)', 'daxpy y=', y
a = reshape([1, 2, 3, 4, 5, 6], [2, 3], order=[1, 2]) * 1.0_dp
b = transpose(a)
v = 0.0_dp
call dgemv('N', 2, 3, 1.0_dp, a, 2, x, 1, 0.0_dp, v, 1) ! v := A x
print '(a, 2f6.1, a, 2f6.1)', 'dgemv v =', v, ' matmul =', matmul(a, x)
c = 0.0_dp
call dgemm('N', 'N', 2, 2, 3, 1.0_dp, a, 2, b, 3, 0.0_dp, c, 2) ! C := A B
print '(a, 4f6.1)', 'dgemm c =', c
print '(a, 4f6.1)', 'matmul =', matmul(a, b)
print '(a, es9.2)', 'max |dgemm - matmul| = ', maxval(abs(c - matmul(a, b)))
end program blas_demo
Output:
ddot = 32.0 dot_product = 32.0
daxpy y= 6.0 9.0 12.0
dgemv v = 22.0 28.0 matmul = 22.0 28.0
dgemm c = 35.0 44.0 44.0 56.0
matmul = 35.0 44.0 44.0 56.0
max |dgemm - matmul| = 0.00E+00
Both compilers give the same output. matmul(a, b) and dgemm agree exactly on this tiny example; with real data they
agree only to rounding, as matmul, dot_product and dgemm shows.
Source: Netlib: BLAS (Basic Linear Algebra Subprograms), J3/23-007r1: 8.5.8.5 Assumed-size array and 15.4.2.2 Explicit interface
How to read an interface:
-
Every scalar argument is passed by reference, as in all Fortran: a literal
1.0_dpforalphais fine because it isintent(in). -
character, intent(in) :: transis a one-character string:'N'for the matrix as it is,'T'for its transpose,'C'for the conjugate transpose (the same as'T'for real data). Upper and lower case are both accepted. -
The data type of the routine is part of its name, so one interface is needed per precision (
sgemmanddgemm). A generic interface can group them under one name; stdlib does exactly that (stdlib_linalg). -
intentis documentation that the compiler enforces on the Fortran side, not something the library knows: the declaredintent(in)foraandintent(inout)forcmatch what the netlib reference says about each argument, and it is the interface writer’s job to get them right. -
The same module can be shared by all callers, which is the reason to put the interfaces in a module and not in each procedure. For a large part of LAPACK, a ready-made module is a better choice than writing it by hand: see stdlib below.
One module for the LAPACK routines
The LAPACK routines used in the rest of this page go in one module, lapack_interfaces, in the same style. The routines are
dgesv (solve a general system), dpotrf and dpotrs (Cholesky factorization and solve), dsyev (symmetric eigenproblem) and
dgemm. The argument lists and the intent of each argument follow the reference implementation’s documentation (info
and ipiv are outputs, a is overwritten by the factors).
module lapack_interfaces
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: dgesv, dpotrf, dpotrs, dsyev, dgemm
interface
subroutine dgesv(n, nrhs, a, lda, ipiv, b, ldb, info)
import :: dp
integer, intent(in) :: n, nrhs, lda, ldb
real(dp), intent(inout) :: a(lda, *), b(ldb, *)
integer, intent(out) :: ipiv(*), info
end subroutine dgesv
subroutine dpotrf(uplo, n, a, lda, info)
import :: dp
character, intent(in) :: uplo
integer, intent(in) :: n, lda
real(dp), intent(inout) :: a(lda, *)
integer, intent(out) :: info
end subroutine dpotrf
subroutine dpotrs(uplo, n, nrhs, a, lda, b, ldb, info)
import :: dp
character, intent(in) :: uplo
integer, intent(in) :: n, nrhs, lda, ldb
real(dp), intent(in) :: a(lda, *)
real(dp), intent(inout) :: b(ldb, *)
integer, intent(out) :: info
end subroutine dpotrs
subroutine dsyev(jobz, uplo, n, a, lda, w, work, lwork, info)
import :: dp
character, intent(in) :: jobz, uplo
integer, intent(in) :: n, lda, lwork
real(dp), intent(inout) :: a(lda, *), work(*)
real(dp), intent(out) :: w(*)
integer, intent(out) :: info
end subroutine dsyev
subroutine dgemm(transa, transb, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc)
import :: dp
character, intent(in) :: transa, transb
integer, intent(in) :: m, n, k, lda, ldb, ldc
real(dp), intent(in) :: alpha, beta, a(lda, *), b(ldb, *)
real(dp), intent(inout) :: c(ldc, *)
end subroutine dgemm
end interface
end module lapack_interfaces
Source: LAPACK reference documentation (dgesv, dpotrf, dpotrs, dsyev), Netlib: BLAS (Basic Linear Algebra Subprograms), J3/23-007r1: 15.4.2 Implicit and explicit interfaces
Every program below that uses LAPACK compiles this file together with its own source and links the library, for example
gfortran -std=f2023 lapack_interfaces.f90 02_dgesv.f90 -lopenblas. Linking is covered in Linking: reference and optimized libraries.
Why the explicit interface matters
Two deliberate mistakes. First, double-precision dgemm called with single-precision arrays, with the explicit
interface in scope (the module is lapack_interfaces, listed above):
! Deliberate error: single-precision arrays passed to the double-precision dgemm.
program wrong_kind
use, intrinsic :: iso_fortran_env, only: sp => real32
use lapack_interfaces, only: dgemm
implicit none
real(sp) :: a(2, 2), b(2, 2), c(2, 2)
a = 1.0_sp
b = 1.0_sp
c = 0.0_sp
call dgemm('N', 'N', 2, 2, 2, 1.0d0, a, 2, b, 2, 0.0d0, c, 2)
end program wrong_kind
GNU Fortran 16.2 rejects the call at compile time, one error for each of the three arrays:
10_wrong_kind.f90:11:64:
11 | call dgemm('N', 'N', 2, 2, 2, 1.0d0, a, 2, b, 2, 0.0d0, c, 2)
| 1
Error: Type mismatch in argument 'a' at (1); passed REAL(4) to REAL(8)
LLVM Flang 23 rejects it as well:
10_wrong_kind.f90:11:41: error: Actual argument type 'REAL(4)' is not compatible with dummy argument type 'REAL(8)'
call dgemm('N', 'N', 2, 2, 2, 1.0d0, a, 2, b, 2, 0.0d0, c, 2)
^
Source: J3/23-007r1: 15.4.2 Implicit and explicit interfaces, Netlib: BLAS (Basic Linear Algebra Subprograms)
Second, the same call written the old way, with external :: dgemm and no interface:
! The same mistake against an implicit interface (old style: external dgemm).
program implicit_iface
use, intrinsic :: iso_fortran_env, only: sp => real32
implicit none
external :: dgemm
real(sp) :: a(2, 2), b(2, 2), c(2, 2)
a = 1.0_sp
b = 1.0_sp
c = 0.0_sp
call dgemm('N', 'N', 2, 2, 2, 1.0d0, a, 2, b, 2, 0.0d0, c, 2)
print *, c
end program implicit_iface
Both compilers accept it, with no diagnostic even under -Wall -Wextra -pedantic, and both print a wrong answer. The
correct product of two matrices of ones is a matrix of 2.0:
0.00000000 0.00000000 0.00000000 0.00000000
LLVM Flang 23 prints different wrong values:
1.701463E+38 .625 1.701463E+38 .625
The library read four-byte data as eight-byte numbers. Nothing in the language or the library can detect this, which is the
reason to use interfaces and never external for a library routine. Source: J3/23-007r1: 15.4.2 Implicit and explicit interfaces
Hidden arguments, integers and threads
Three details of the binary interface are worth knowing, and none shows up in the Fortran source:
-
String lengths. A
characterdummy argument of a Fortran 77 routine is passed with a hidden length argument, which GNU Fortran documents under argument-passing conventions. A library built with another compiler may follow different conventions for that hidden argument; a Fortran compiler and a library from the same toolchain family, or a library that documents its Fortran calling convention, are safe. With an explicit interface the compiler generates the right call for its own convention. -
Integer kind. The routines above take default integers, 32 bits. Libraries also exist in a 64-bit integer flavour (called ILP64) for arrays with more than 2^31 elements; they are separate builds with their own link names, and the Fortran side must use 64-bit integers in the interface to match. GNU Fortran’s
-fexternal-blas64(see matmul, dot_product and dgemm) is the compiler’s switch for the same issue. Read the documentation of the library for the exact names; this page does not use them. -
Threads. OpenBLAS runs
dgemmand the other large routines on several threads by default. The environment variableOPENBLAS_NUM_THREADSsets the number, and the measurements below depend on it. A program that is itself parallel with OpenMP or MPI (OpenMP and OpenACC, MPI with mpi_f08) has to avoid multiplying the two thread counts: one library thread per OpenMP thread or MPI rank is the usual starting point.
Calling on a sub-block: sequence association
To multiply a block of a larger matrix, pass the first element of the block and the leading dimension of the whole array.
An array element passed to an assumed-size or explicit-shape dummy argument starts a sequence of elements in array element
order (sequence association, 15.5.2.12), so a(2, 2) stands for "the array that begins at row 2 of column 2" and lda = 4
makes the routine step over the rest of each column. This is the tool that makes LAPACK’s blocked algorithms possible, and
it is the one place where passing a section such as a(2:3, 2:3) instead is wrong: a section that is not contiguous is
copied into a temporary array whose leading dimension is 2, not 4.
program submatrix
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dgemm
implicit none
real(dp) :: a(4, 4), b(4, 4), c(4, 4), ref(2, 2)
integer :: i, j
do j = 1, 4
do i = 1, 4
a(i, j) = real(10*i + j, dp)
end do
end do
b = a
c = 0.0_dp
! C(2:3,2:3) := A(2:3,2:3) * B(2:3,2:3). Pass the first element of each block (sequence association)
! and the leading dimension of the PARENT array: lda = ldb = ldc = 4.
call dgemm('N', 'N', 2, 2, 2, 1.0_dp, a(2, 2), 4, b(2, 2), 4, 0.0_dp, c(2, 2), 4)
ref = matmul(a(2:3, 2:3), b(2:3, 2:3))
print '(a, es9.2)', 'block product, max difference to matmul: ', maxval(abs(c(2:3, 2:3) - ref))
print '(a, es9.2)', 'everything outside the block still zero: ', sum(abs(c)) - sum(abs(c(2:3, 2:3)))
end program submatrix
The listing uses the same lapack_interfaces module as the sections that follow (it is listed above). Output:
block product, max difference to matmul: 0.00E+00
everything outside the block still zero: 0.00E+00
Solving a linear system: dgesv
dgesv solves a general square system A x = b by LU factorization with partial pivoting. It overwrites a with the factors,
b with the solution, and ipiv receives the row interchanges. The example is one implicit heat step on a rod of five
points with a hot spot in the middle: with r = alpha dt / dx^2, the system is (I - r L) u_new = u_old, where L is the second-difference
matrix and the two end values are held at zero.
! One implicit (backward Euler) heat step on a 1D rod: (I - r*L) u_new = u_old,
! with L the second-difference matrix and Dirichlet ends, solved by dgesv.
program implicit_rod
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dgesv
implicit none
integer, parameter :: n = 5
real(dp), parameter :: r = 0.4_dp ! alpha*dt/dx**2
real(dp) :: a(n, n), u(n, 1), a0(n, n), u0(n, 1)
integer :: ipiv(n), info, i
a = 0.0_dp
do i = 1, n
a(i, i) = 1.0_dp + 2.0_dp*r
end do
do i = 1, n - 1
a(i + 1, i) = -r
a(i, i + 1) = -r
end do
u = 0.0_dp
u(3, 1) = 100.0_dp ! hot spot in the middle
a0 = a
u0 = u
call dgesv(n, 1, a, n, ipiv, u, n, info) ! a is overwritten by its LU factors, u by the solution
print '(a, i0)', 'info = ', info
print '(a, 5i4)', 'ipiv =', ipiv
print '(a, 5f9.4)', 'u =', u(:, 1)
print '(a, es10.2)', 'max residual |A0*u - u0| = ', maxval(abs(matmul(a0, u(:, 1)) - u0(:, 1)))
end program implicit_rod
Output (identical with GNU Fortran 16.2 and LLVM Flang 23):
info = 0
ipiv = 1 2 3 4 5
u = 3.2206 14.4928 61.9968 14.4928 3.2206
max residual |A0*u - u0| = 2.80E-15
The solution is the expected smoothed hot spot, and the residual
maxval(abs(a0*u - u0)) is at rounding level: always compute it, because info = 0 only says that the factorization
completed, not that the answer is accurate.
Info codes and error reporting
LAPACK reports problems through its info argument and, for illegal arguments, through the routine xerbla. The
convention is the same for every routine, and every call should check it:
info |
Meaning |
|---|---|
|
successful exit |
|
the argument number |
|
the algorithm failed for a reason that depends on the data, and the meaning is routine specific: |
The info > 0 case is the one that depends on the numbers. Two matrices, one of them singular only on paper:
program singular
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dgesv
implicit none
real(dp) :: a(3, 3), b(3, 1)
integer :: ipiv(3), info
! Rows 1 2 3 / 4 5 6 / 7 8 9: singular on paper, but rounding hides it from the factorization.
a = reshape([1, 2, 3, 4, 5, 6, 7, 8, 9], [3, 3], order=[2, 1]) * 1.0_dp
b = 1.0_dp
call dgesv(3, 1, a, 3, ipiv, b, 3, info)
print '(a, i0, a, es10.2)', 'rows 1..9: info = ', info, ' |u(3,3)| = ', abs(a(3, 3))
! Rows 1 2 3 / 2 4 6 / 1 1 1: the second row is twice the first and the elimination is exact.
a = reshape([1, 2, 3, 2, 4, 6, 1, 1, 1], [3, 3], order=[2, 1]) * 1.0_dp
b = 1.0_dp
call dgesv(3, 1, a, 3, ipiv, b, 3, info)
print '(a, i0)', 'rows 2x: info = ', info
if (info > 0) print '(a, i0, a, i0, a)', 'U(', info, ',', info, ') is exactly zero: the matrix is singular, no solution was computed'
end program singular
Output (identical with both compilers):
rows 1..9: info = 0 |u(3,3)| = 1.59E-16
rows 2x: info = 3
U(3,3) is exactly zero: the matrix is singular, no solution was computed
The first matrix has rank 2, and info is still zero: rounding during the elimination leaves a last pivot of order 10^-16 and not
exactly zero. The routine cannot know that this number is "really" zero. info > 0 is a report about exact zeros only;
for ill-conditioned systems the check to rely on is the condition number (LAPACK offers dgecon, and the expert driver
dgesvx returns an estimate) or the residual, as in the previous section.
An illegal argument is a different kind of failure. Here lda = 2 is smaller than the matrix order 3:
program illegal
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dgesv
implicit none
real(dp) :: a(3, 3), b(3, 1)
integer :: ipiv(3), info
a = 0.0_dp
a(1, 1) = 1.0_dp; a(2, 2) = 1.0_dp; a(3, 3) = 1.0_dp
b = 1.0_dp
info = 99
call dgesv(3, 1, a, 2, ipiv, b, 3, info) ! lda = 2 < max(1, n) is an error
print '(a, i0)', 'back from dgesv, info = ', info
end program illegal
With OpenBLAS the library prints a message from xerbla and returns, with info set to the negated argument number:
** On entry to DGESV parameter number 4 had an illegal value
back from dgesv, info = -4
The message is written to standard error and the program carries on. LLVM Flang 23 prints the same two lines, in the other order,
because standard output is buffered when it is a pipe. The behaviour of xerbla belongs to the library. The reference
xerbla (in the BLAS directory of the netlib sources) prints the same message and then executes a plain stop. Linking
the same program statically against the netlib liblapack.a and libblas.a of the container printed only the line
** On entry to DGESV parameter number 4 had an illegal value and never reached the print after the call; the process exit
status was 0, because a plain stop reports success. A program that has to survive a bad argument must check info, and a
program that has to detect it everywhere must run its tests against the reference
library as well as the optimized one.
A worked concept: the Cholesky factorization
A real symmetric matrix A is positive definite if x^T A x > 0 for every nonzero x. Such a matrix has a unique factorization
where L is lower triangular with a positive diagonal, called the Cholesky factor. It is the fastest and most stable way of solving systems with positive definite matrices (about half the work of an LU factorization, and no pivoting), and the standard test for positive definiteness is to try it and see whether it fails. Positive definite matrices are everywhere in simulation: the implicit heat step above, covariance matrices, stiffness matrices and normal equations are all of this kind. Column by column, for j = 1, …, n:
If the quantity under the square root is not positive, A is not positive definite. The program below does three
things: it codes the formulas with whole-array dot_product calls, it calls LAPACK’s dpotrf on the same matrix and
compares the factors, and it uses dpotrs to solve with the factor. The matrix is the implicit rod step with four points.
module cholesky_mod
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: cholesky_lower
contains
!> Cholesky-Banachiewicz: A = L * transpose(L), L lower triangular. info > 0: leading minor info is not positive.
subroutine cholesky_lower(a, l, info)
real(dp), intent(in) :: a(:, :)
real(dp), intent(out) :: l(:, :)
integer, intent(out) :: info
integer :: i, j, n
real(dp) :: s
n = size(a, 1)
l = 0.0_dp
info = 0
do j = 1, n
s = a(j, j) - dot_product(l(j, 1:j - 1), l(j, 1:j - 1))
if (s <= 0.0_dp) then
info = j
return
end if
l(j, j) = sqrt(s)
do i = j + 1, n
l(i, j) = (a(i, j) - dot_product(l(i, 1:j - 1), l(j, 1:j - 1))) / l(j, j)
end do
end do
end subroutine cholesky_lower
end module cholesky_mod
program cholesky_demo
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dpotrf, dpotrs
use cholesky_mod, only: cholesky_lower
implicit none
integer, parameter :: n = 4
real(dp), parameter :: r = 0.4_dp
real(dp) :: a(n, n), l_own(n, n), l_lap(n, n), b(n, 1), x(n, 1), bad(2, 2)
integer :: i, j, info
! The matrix of an implicit heat step, I - r*L: symmetric and positive definite.
a = 0.0_dp
do i = 1, n
a(i, i) = 1.0_dp + 2.0_dp*r
end do
do i = 1, n - 1
a(i + 1, i) = -r
a(i, i + 1) = -r
end do
call cholesky_lower(a, l_own, info)
print '(a, i0)', 'own Cholesky, info = ', info
l_lap = a
call dpotrf('L', n, l_lap, n, info) ! lower triangle overwritten by L; upper left untouched
print '(a, i0)', 'dpotrf, info = ', info
do j = 1, n
do i = 1, j - 1
l_lap(i, j) = 0.0_dp ! drop the untouched upper triangle before comparing
end do
end do
print '(a, es10.2)', 'max |L_own - L_lapack| = ', maxval(abs(l_own - l_lap))
print '(a, es10.2)', 'max |L*L^T - A| = ', maxval(abs(matmul(l_own, transpose(l_own)) - a))
b(:, 1) = [0.0_dp, 100.0_dp, 100.0_dp, 0.0_dp]
x = b
call dpotrs('L', n, 1, l_lap, n, x, n, info) ! two triangular solves with the factor
print '(a, 4f9.4)', 'x = ', x(:, 1)
print '(a, es10.2)', 'max |A*x - b| = ', maxval(abs(matmul(a, x(:, 1)) - b(:, 1)))
bad = reshape([1.0_dp, 2.0_dp, 2.0_dp, 1.0_dp], [2, 2]) ! eigenvalues 3 and -1
call dpotrf('L', 2, bad, 2, info)
print '(a, i0, a)', 'indefinite matrix: dpotrf info = ', info, ' (leading minor of that order is not positive)'
end program cholesky_demo
Output (identical with GNU Fortran 16.2 and LLVM Flang 23):
own Cholesky, info = 0
dpotrf, info = 0
max |L_own - L_lapack| = 5.55E-17
max |L*L^T - A| = 2.22E-16
x = 16.9492 76.2712 76.2712 16.9492
max |A*x - b| = 1.42E-14
indefinite matrix: dpotrf info = 2 (leading minor of that order is not positive)
What to take from it:
-
The two factors agree to rounding, and
dpotrfwas called once, not once per right-hand side. The usual pattern isdpotrfto factorize, thendpotrsfor each new right-hand side: solving is O(n^2) once the factor exists. -
dpotrfwithuplo = 'L'references and overwrites only the lower triangle; the upper triangle of the array keeps the old values, which is why the example zeroes it before comparing. Theuploargument says which triangle of the symmetric matrix is stored, and the other one is never read. -
For the indefinite matrix with eigenvalues 3 and -1, the factorization stops with
info = 2: the leading minor of order 2 is not positive. This is how to test positive definiteness. -
LAPACK’s
dpotrfis a blocked algorithm: its documentation says "This is the block version of the algorithm, calling Level 3 BLAS". It works on panels of columns so that most of the arithmetic is in matrix-matrix calls. The hand-written loop is the same arithmetic done one column at a time, and it is the right size for a lesson and the wrong one for a large matrix. -
The matrix of the implicit step is also tridiagonal. Storing the full n by n array and factorizing it costs O(n^3); the tridiagonal routine
dptsvuses only the diagonal and the sub-diagonal, O(n) storage and O(n) work, and returns the same answer:
! The implicit rod step again, now with the tridiagonal structure exposed: O(n) work and O(n) storage.
module tridiag_interfaces
use, intrinsic :: iso_fortran_env, only: dp => real64
implicit none
private
public :: dptsv
interface
subroutine dptsv(n, nrhs, d, e, b, ldb, info)
import :: dp
integer, intent(in) :: n, nrhs, ldb
real(dp), intent(inout) :: d(*), e(*), b(ldb, *)
integer, intent(out) :: info
end subroutine dptsv
end interface
end module tridiag_interfaces
program tridiag
use, intrinsic :: iso_fortran_env, only: dp => real64
use tridiag_interfaces, only: dptsv
implicit none
integer, parameter :: n = 5
real(dp), parameter :: r = 0.4_dp
real(dp) :: d(n), e(n - 1), u(n, 1)
integer :: info
d = 1.0_dp + 2.0_dp*r ! diagonal
e = -r ! sub-diagonal (the matrix is symmetric positive definite)
u = 0.0_dp
u(3, 1) = 100.0_dp
call dptsv(n, 1, d, e, u, n, info) ! A = L D L^T for symmetric positive definite tridiagonal A
print '(a, i0)', 'info = ', info
print '(a, 5f9.4)', 'u =', u(:, 1)
end program tridiag
info = 0
u = 3.2206 14.4928 61.9968 14.4928 3.2206
(The output is identical with LLVM Flang 23, and matches the dgesv solution of the previous section.) Choose the
routine for the structure of the matrix: general (ge), symmetric positive definite (po), tridiagonal (gt, pt) or banded (gb,
pb). The structure-specific routines are faster and use less memory.
Workspace queries
Several LAPACK routines need a scratch array work of a size that depends on the problem and on the block size the
library considers best. The routine tells the size itself: call it with lwork = -1 and it computes the optimal length, stores
it in work(1) and does nothing else. Allocate that much and call again.
program eigen
use, intrinsic :: iso_fortran_env, only: dp => real64
use lapack_interfaces, only: dsyev
implicit none
integer, parameter :: n = 4
real(dp) :: a(n, n), w(n), wquery(1)
real(dp), allocatable :: work(:)
integer :: lwork, info, i
a = 0.0_dp
do i = 1, n
a(i, i) = 2.0_dp
end do
do i = 1, n - 1
a(i + 1, i) = -1.0_dp
a(i, i + 1) = -1.0_dp
end do
call dsyev('V', 'L', n, a, n, w, wquery, -1, info) ! lwork = -1: workspace query, nothing is computed
lwork = int(wquery(1))
print '(a, i0, a, i0)', 'optimal lwork = ', lwork, ' minimum = ', 3*n - 1
allocate (work(lwork))
call dsyev('V', 'L', n, a, n, w, work, lwork, info)
print '(a, i0)', 'info = ', info
print '(a, 4f9.5)', 'eigenvalues ', w
print '(a, 4f9.5)', 'exact ', [(2.0_dp - 2.0_dp*cos(real(i, dp)*acos(-1.0_dp)/5.0_dp), i=1, 4)]
end program eigen
The test matrix is the one-dimensional second-difference matrix, whose eigenvalues are 2 - 2 cos(k pi / 5). Output (identical with both compilers):
optimal lwork = 136 minimum = 11
info = 0
eigenvalues 0.38197 1.38197 2.61803 3.61803
exact 0.38197 1.38197 2.61803 3.61803
The documented minimum for dsyev is lwork >= max(1, 3*n - 1), which is 11 for n = 4. The optimal value is much larger on
OpenBLAS, 136; using the minimum would work and be slower. The query is cheap, and a function in the program that owns the
work array can do it once and keep the array. Using an allocatable work array is the modern replacement for the fixed-size
scratch arrays and the lwork arithmetic of old code.
Linking: reference and optimized libraries
Linking is where most of the time is lost with numerical libraries, because nothing in the source says which BLAS the program gets. The options, all of which provide the same interface:
| Library | What it is | How to link |
|---|---|---|
Reference BLAS and LAPACK (netlib) |
the Fortran reference implementation; correct, portable, slow (see the measurements below) |
|
OpenBLAS |
open source optimized BLAS that also carries a LAPACK, with multithreading and tuned kernels for many processors |
|
Intel oneMKL |
Intel’s optimized BLAS, LAPACK, FFT and other math routines; the link line depends on compiler, threading layer and integer width, and Intel supplies a link-line advisor to generate it |
see the oneMKL documentation and its link-line advisor |
Vendor libraries |
for example Accelerate on macOS, or the libraries of an HPC vendor |
see the vendor documentation |
Two practical points. First, on a system with the Debian alternatives mechanism, the same reference-style link line
-lblas -llapack may end up with OpenBLAS at run time: in the container used for this page libblas.so.3 is a link managed by
update-alternatives and points to the OpenBLAS build. That is a feature of the packaging (a program can be switched
to another implementation without relinking), and a surprise when timing the reference library. To measure the netlib code the
page links libblas.a statically. Second, link order matters on Unix linkers: a library must appear before the libraries it
depends on, so -llapack -lblas, never -lblas -llapack.
gfortran
The examples in this page are compiled and linked like this, the source files first and the libraries last:
gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all lapack_interfaces.f90 02_dgesv.f90 -lopenblas -o dgesv
flang-23 lapack_interfaces.f90 02_dgesv.f90 -lopenblas -o dgesv
fpm
fpm links external libraries with the link key of the [build] table of the manifest (and it can also be given per
executable, example or test target). The manifest documentation says that when libraries depend on each other the order
matters, and gives the LAPACK-before-BLAS example. This project is eigen_demo: src/lapack_interfaces.f90 holds the
module, app/main.f90 is the eigenvalue program above.
name = "eigen_demo"
version = "0.1.0"
[build]
auto-executables = true
link = ["lapack", "blas"]
fpm run built and ran it (fpm 0.13.0), with the same output as the direct build:
optimal lwork = 136 minimum = 11
info = 0
eigenvalues 0.38197 1.38197 2.61803 3.61803
exact 0.38197 1.38197 2.61803 3.61803
The library can also be chosen on the command line without the link key: with the key removed from the manifest, fpm run --link-flag "-lopenblas" built and ran the same program with the same output.
CMake
CMake’s FindLAPACK module finds a LAPACK and the BLAS it needs, and exports an imported target LAPACK::LAPACK that
carries the right link line. BLA_VENDOR selects a specific implementation instead of the first one found.
cmake_minimum_required(VERSION 3.20)
project(eigen_demo LANGUAGES Fortran)
find_package(LAPACK REQUIRED) # also finds BLAS; sets LAPACK_LIBRARIES
add_library(lapack_interfaces lapack_interfaces.f90)
add_executable(eigen eigen.f90)
target_link_libraries(eigen PRIVATE lapack_interfaces LAPACK::LAPACK)
message(STATUS "LAPACK libraries: ${LAPACK_LIBRARIES}")
Configured with CMake 3.31.6 and Ninja in the container, the default search found OpenBLAS, and with
-DBLA_VENDOR=OpenBLAS it found the same library; -DBLA_VENDOR=Generic found the reference libraries:
-- Found BLAS: /usr/lib/aarch64-linux-gnu/libopenblas.so
-- Found LAPACK: /usr/lib/aarch64-linux-gnu/libopenblas.so;/usr/lib/aarch64-linux-gnu/libopenblas.so
-- LAPACK libraries: /usr/lib/aarch64-linux-gnu/libopenblas.so;/usr/lib/aarch64-linux-gnu/libopenblas.so
-- Found BLAS: /usr/lib/aarch64-linux-gnu/libblas.so
-- Found LAPACK: /usr/lib/aarch64-linux-gnu/liblapack.so;/usr/lib/aarch64-linux-gnu/libblas.so
(The second block is the Generic search; as explained above, libblas.so on this system is itself an alternatives link.) The built
program printed the same eigenvalues. FindLAPACK and FindBLAS also have BLA_STATIC, BLA_SIZEOF_INTEGER for choosing 64-bit
integer libraries, and a BLA_VENDOR list that includes OpenBLAS, Intel MKL, Apple and generic builds; the module
documentation lists them. The build tools themselves are covered in
Building with Make, CMake and Meson and
Fortran Package Manager (fpm).
matmul, dot_product and dgemm
The language’s own matmul (16.9.134) and dot_product (16.9.73) intrinsics compute the same mathematical products as
dgemm and ddot, and they are always available. The standard defines the result and nothing about how it is computed, so
how fast matmul is, and whether it uses a BLAS, is a quality-of-implementation matter. The differences that matter:
| Aspect | matmul, dot_product |
dgemm, ddot |
|---|---|---|
Interface |
intrinsic: any numeric kind and shape, shape checked at run time with |
external: one precision per routine, shapes and leading dimensions passed by hand |
Result |
a new array, written as an expression |
written into |
Temporaries |
a function result; the compiler may need a temporary when the target aliases an operand |
none: the output array is passed in |
Sub-blocks |
slices, |
first element plus leading dimension (previous section) |
Speed |
the compiler’s own algorithm, unless told to call a BLAS (below) |
tuned for the machine; multithreaded in OpenBLAS and oneMKL |
Rounding |
not specified; the summation order is the library’s |
not specified either: two BLASes can differ in the last digits |
Reference BLAS, OpenBLAS and matmul were timed on one machine (an Apple-silicon Mac running the aarch64 container, 10 cores
available to it, GNU Fortran 16.2 with -O2), multiplying two 1000 by 1000 double precision matrices with this program:
program bench
use, intrinsic :: iso_fortran_env, only: dp => real64, int64
use lapack_interfaces, only: dgemm
implicit none
integer, parameter :: n = 1000
real(dp), allocatable :: a(:, :), b(:, :), c1(:, :), c2(:, :)
integer(int64) :: t0, t1, t2, rate
allocate (a(n, n), b(n, n), c1(n, n), c2(n, n))
call random_number(a)
call random_number(b)
call system_clock(t0, rate)
c1 = matmul(a, b)
call system_clock(t1)
call dgemm('N', 'N', n, n, n, 1.0_dp, a, n, b, n, 0.0_dp, c2, n)
call system_clock(t2)
print '(a, f8.3, a)', 'matmul : ', real(t1 - t0, dp)/real(rate, dp), ' s'
print '(a, f8.3, a)', 'dgemm : ', real(t2 - t1, dp)/real(rate, dp), ' s'
print '(a, es9.2)', 'max |matmul - dgemm| = ', maxval(abs(c1 - c2))
end program bench
| Build | matmul (s) |
dgemm (s) |
|---|---|---|
reference BLAS, static netlib |
0.088 |
0.519 |
OpenBLAS, 10 threads |
0.087 |
0.051 |
OpenBLAS, |
0.086 |
0.071 |
OpenBLAS with |
0.033 |
0.021 |
reference BLAS with |
0.418 |
0.381 |
LLVM Flang 23 |
0.291 |
0.037 |
These numbers come from single runs and vary: the multithreaded OpenBLAS dgemm took between 0.014 s and 0.051 s in
repeated runs, mostly because of thread start-up, and the matmul figures between 0.049 s and 0.088 s. The ratios are
what to read. The reference dgemm was about six times slower than GNU Fortran’s own matmul (the netlib code is not meant for speed);
the optimized dgemm was faster than matmul by about 1.2 times with one thread and between 1.7 and about 4 times
with all the cores, not by a factor of a hundred; matmul in LLVM Flang 23 was several times slower than in GNU Fortran. These are measurements of one
machine and one matrix size. A different processor, size or thread count changes them, so measure on the machine that matters before
deciding, as described in Performance and Memory Layout.
GNU Fortran can route matmul to a BLAS without a source change. -fexternal-blas makes the compiler generate calls to BLAS
for some matrix operations like matmul when the matrices are larger than a limit, -fblas-matmul-limit=n sets that limit
(it compares the size, or the geometric mean of the dimensions if the matrices are not square, with n), and -fexternal-blas64 is the
variant for a BLAS with 64-bit integers (it also requires -ffrontend-optimize); the BLAS library has to be named at link
time. The table shows the effect: with OpenBLAS matmul became about 2.6 times faster and came close to the dgemm time, and with the reference
BLAS it became slower, since the reference dgemm is slower than the compiler’s own code. The option exists to get the benefit of an optimized BLAS in
existing code and is only sensible when one is linked. In LLVM Flang 23.1.4 neither flang --help nor --help-hidden lists an
option with BLAS in its description, so there is nothing equivalent to check.
stdlib_linalg
Writing interfaces by hand is tedious, so the fortran-lang standard library provides them. stdlib_linalg is a high-level,
kind-generic interface to linear algebra built on BLAS and LAPACK: solve, lstsq, det, inv, chol, eigh, eig, svd,
qr, schur, norms and helpers such as diag, eye, trace and is_symmetric. The module and its status are documented in the
stdlib specification; individual procedures are marked there as stable or experimental, so check the status before
depending on one. The linalg page also documents stdlib_linalg_blas and stdlib_linalg_lapack, a Modern Fortran
translation of reference LAPACK 3.10.1 (free format, implicit none, explicit intent) with kind-generic names (gemm
alongside the original stdlib_dgemm). The documentation says that optimized libraries should be preferred when available
and that defining the macros STDLIB_EXTERNAL_BLAS and STDLIB_EXTERNAL_LAPACK when building stdlib wraps the external
libraries instead of the built-in translation.
Used from fpm, with the stdlib = "*" registry dependency and an external BLAS to link; the project below is the manifest:
name = "linalg_demo"
version = "0.1.0"
license = "MIT"
[dependencies]
stdlib = "*"
[build]
link = ["openblas"]
auto-executables = true
On 2026-10-19 the registry dependency resolved to stdlib 0.8.1 (the stdlib-fpm branch with pre-processed sources). The
program solves the implicit heat step, factorizes it, computes eigenvalues and checks the singular case with the error type
linalg_state_type:
program main
use, intrinsic :: iso_fortran_env, only: dp => real64
use stdlib_linalg, only: solve, chol, eigh, det, inv
use stdlib_linalg_state, only: linalg_state_type
implicit none
integer, parameter :: n = 4
real(dp), parameter :: r = 0.4_dp
real(dp) :: a(n, n), b(n), l(n, n), lam(n)
real(dp), allocatable :: x(:), vec(:, :)
real(dp) :: sing(2, 2)
type(linalg_state_type) :: err
integer :: i
a = 0.0_dp
do i = 1, n
a(i, i) = 1.0_dp + 2.0_dp*r
end do
do i = 1, n - 1
a(i + 1, i) = -r
a(i, i + 1) = -r
end do
b = [0.0_dp, 100.0_dp, 100.0_dp, 0.0_dp]
x = solve(a, b)
print '(a, 4f9.4)', 'solve: x = ', x
l = chol(a, lower=.true.)
print '(a, es10.2)', 'chol: max |L*L^T - A| = ', maxval(abs(matmul(l, transpose(l)) - a))
call eigh(a, lam, vectors=vec)
print '(a, 4f9.5)', 'eigh: lambda = ', lam
print '(a, f9.5, a, f9.5)', 'det = ', det(a), ' product of eigenvalues = ', product(lam)
print '(a, es10.2)', 'inv: max |A*inv(A) - I| = ', maxval(abs(matmul(a, inv(a)) - eye()))
sing = reshape([1.0_dp, 2.0_dp, 2.0_dp, 4.0_dp], [2, 2])
x = solve(sing, [1.0_dp, 1.0_dp], err=err)
print '(a, l1)', 'singular system: err%error() = ', err%error()
print '(a)', err%print()
contains
function eye() result(e)
real(dp) :: e(n, n)
integer :: k
e = 0.0_dp
do k = 1, n
e(k, k) = 1.0_dp
end do
end function eye
end program main
Built with GNU Fortran 16.2 through fpm (fpm’s default flags, not the strict -std=f2023 line):
solve: x = 16.9492 76.2712 76.2712 16.9492
chol: max |L*L^T - A| = 2.22E-16
eigh: lambda = 1.15279 1.55279 2.04721 2.44721
det = 8.96800 product of eigenvalues = 8.96800
inv: max |A*inv(A) - I| = 2.22E-16
singular system: err%error() = T
[solve] returned Algebra Error: singular matrix
The results agree with the hand-written dgesv/dpotrf examples above (the solution x = 16.9492 76.2712 ..., the same Cholesky
residual at rounding level), and the product of the eigenvalues equals the determinant. Things to know about the interface:
-
Functions return new arrays (
x = solve(a, b)), which is convenient for a first program and allocates on every call. Subroutine forms exist for most operations, and are the way to avoid the temporaries in a loop. -
Every procedure takes an optional
errargument of typelinalg_state_type. Without it, an error (a singular matrix, for instance) stops the program; with it, the program continues and can testerr%error()and printerr%print(). -
cholis the function andcholeskyis the subroutine interface in this release; callingcholeskyas a function gives the compile error “'cholesky' at (1) is not a function”. -
The kind-generic BLAS names remove the
dandsprefixes and the interface-writing: the next program callsgemmanddotwithreal64andreal32arguments.
program stdlib_gemm
use, intrinsic :: iso_fortran_env, only: dp => real64, sp => real32
use stdlib_linalg_blas, only: gemm, dot
implicit none
real(dp) :: a(2, 3), b(3, 2), c(2, 2)
real(sp) :: x(3), y(3)
a = reshape([1, 2, 3, 4, 5, 6], [2, 3], order=[2, 1]) * 1.0_dp
b = transpose(a)
c = 0.0_dp
call gemm('N', 'N', 2, 2, 3, 1.0_dp, a, 2, b, 3, 0.0_dp, c, 2) ! generic name: no d prefix, no explicit interface to write
print '(a, 4f6.1)', 'gemm (real64): ', c
x = [1.0, 2.0, 3.0]
y = [4.0, 5.0, 6.0]
print '(a, f6.1)', 'dot (real32): ', dot(3, x, 1, y, 1)
end program stdlib_gemm
gemm (real64): 14.0 32.0 32.0 77.0
dot (real32): 32.0
stdlib was built and run with GNU Fortran only; the same examples were not built with Flang through fpm. The package itself is described in Fortran stdlib.
Other numerical libraries
Libraries for other jobs follow the same pattern as BLAS and LAPACK: a compiled library, an interface to write or include, and a link line. Most are written in C and give Fortran a thin binding. Those named below are well-known community and vendor projects, not part of the language.
| Library | Purpose | Fortran interface |
|---|---|---|
FFTW |
fast Fourier transforms, 1D to 3D, real and complex |
Fortran 2003 interface in the header |
HDF5 |
self-describing hierarchical data files for large arrays |
a Fortran API in the HDF5 distribution (module |
NetCDF |
array-oriented scientific data files |
NetCDF-Fortran, a separate library layered over NetCDF-C |
PETSc |
parallel solvers for large sparse linear and nonlinear systems, built on MPI |
Fortran bindings in the distribution; the PETSc manual has a Fortran chapter |
(HDF5, NetCDF and PETSc were not installed or run for this page; the table lists what each project documents, with the project pages in the references.) The Fortran 2003 FFTW interface is the one to look at, because it shows the C interoperability tools doing real work; see Interoperability with C. This program transforms a row of 16 samples containing three waves and prints the bins with energy:
program fft_row
use, intrinsic :: iso_c_binding
implicit none
include 'fftw3.f03'
integer, parameter :: n = 16
real(c_double), parameter :: pi = acos(-1.0_c_double)
real(c_double), pointer :: x(:)
complex(c_double_complex), pointer :: y(:)
type(c_ptr) :: px, py, plan
integer :: i
! FFTW wants its own aligned memory: fftw_alloc_real / fftw_alloc_complex and c_f_pointer
px = fftw_alloc_real(int(n, c_size_t))
py = fftw_alloc_complex(int(n/2 + 1, c_size_t))
call c_f_pointer(px, x, [n])
call c_f_pointer(py, y, [n/2 + 1])
plan = fftw_plan_dft_r2c_1d(n, x, y, FFTW_ESTIMATE) ! plan first: planning may overwrite the arrays
do i = 0, n - 1
x(i + 1) = sin(2.0_c_double*pi*3.0_c_double*real(i, c_double)/real(n, c_double)) ! a row with three waves
end do
call fftw_execute_dft_r2c(plan, x, y)
do i = 0, n/2
if (abs(y(i + 1)) > 1.0e-10_c_double) print '(a, i0, a, f8.3)', 'bin ', i, ': |y| = ', abs(y(i + 1))
end do
call fftw_destroy_plan(plan)
call fftw_free(px)
call fftw_free(py)
end program fft_row
Compiled with gfortran 09_fftw.f90 -I/usr/include -lfftw3 and the same flags for Flang; both print
bin 3: |y| = 8.000
The bin is 3 and its magnitude is 8, which is n/2 for a unit-amplitude wave. Three details: the include 'fftw3.f03' file is found
through -I, the plan is made before the data is written (planning with some flags overwrites the arrays), and the arrays come from
fftw_alloc_real so that they are aligned for the vector instructions. With -Wall -Wextra the vendor header produces about forty
-Wunused-parameter warnings (one per constant it declares that the program does not use), which is a property of
the header, not of the program, and -Wno-unused-parameter silences them.
Where the books are outdated
The two Fortran books consulted for this section treat numerical libraries as an application of the language and touch BLAS and LAPACK only through worked examples. Where older teaching material differs from current practice:
| Area | Older practice, and the current approach |
|---|---|
Interfaces |
older code calls BLAS and LAPACK as |
Types |
Fortran 77 style declarations ( |
Intrinsics |
hand-written triple loops for matrix products and ad hoc dot products; use |
Build |
link lines copied by hand; use fpm’s |
Libraries |
reference LAPACK as "the" LAPACK; it is the specification and the reference, and OpenBLAS or oneMKL are what programs link in practice |
Higher-level packages |
no standard-library linear algebra; |
Standard version provenance and compiler support
| Feature | Defined in J3/23-007r1 | Compiler support checked on 2026-10-19 |
|---|---|---|
|
16.9.134 and 16.9.73 |
GNU Fortran 16.2 and LLVM Flang 23 accept the examples |
Interface bodies and |
15.4.3.2 and 8.8 |
both |
Explicit versus implicit interfaces |
15.4.2 |
both accept an implicit interface and diagnose nothing on a kind mismatch (above) |
Assumed-size dummy arguments |
8.5.8.5 and 15.5.2.12 |
both |
|
18.2 |
both build and run the FFTW example |
|
8.7 |
not used in the examples; |
BLAS and LAPACK calls, library linking |
not part of the standard |
OpenBLAS 0.3.29 and reference BLAS/LAPACK 3.12.1 linked by both compilers |
|
GNU Fortran options, not in the standard |
GNU Fortran only; no equivalent found in LLVM Flang 23 |
Not checked here, because no compiler for them is installed: Intel ifx, NAG, HPE Cray and NVIDIA HPC compilers; and oneMKL, which was not installed or run. See History, Standards and Compilers for the per-compiler support table.
Pitfalls
-
External without an interface. The compiler accepts any arguments, and a kind mismatch produces wrong numbers without a diagnostic (see above). Always
usean interface module. -
Passing a section instead of the first element. A non-contiguous section is copied to a temporary with a different leading dimension. For a block of a larger matrix pass
a(i, j)and the leading dimension of the parent. -
A leading dimension smaller than the number of rows. An illegal argument, reported by
xerbla: with some libraries it prints and continues, with the reference library it stops. -
Ignoring
info.info = 0is the only success; checkinfo < 0as a bug andinfo > 0as a data condition. -
Believing
info = 0means an accurate answer. The singular1..9matrix passed. Compute a residual or a condition estimate. -
Forgetting that the routines overwrite their inputs.
abecomes the factors andbthe solution; keep a copy (the residual check needsa0). -
Mixing precisions.
sgemmanddgemmare different routines; usereal(dp)throughout and let the interface catch the rest. -
Wrong link order.
-llapack -lblas, not the other way round.-lopenblasalone is enough for OpenBLAS. -
Not knowing which BLAS ran. Alternatives,
LD_LIBRARY_PATHand a staticlibblas.acan each change it. Print the link line, or runlddon the program, before timing it. -
Thread oversubscription. Library threads times OpenMP threads times MPI ranks; set
OPENBLAS_NUM_THREADSexplicitly. -
Timing one run. The threaded
dgemmvaried by a factor of three here. Repeat, and report medians. -
Row-major thinking. BLAS and LAPACK are column-major, as Fortran is; a C caller that forgets this transposes everything.
-
Comparing
matmulanddgemmresults with==. They differ in the last digits; compare with a tolerance.
See also Best Practices and Style and Common Gotchas and Pitfalls.
References
-
Netlib: LAPACK (Linear Algebra PACKage), LAPACK Users' Guide: Naming Scheme and LAPACK reference documentation
-
OpenBLAS and OpenBLAS FAQ
-
GNU Fortran manual: Options for code generation conventions and Argument passing conventions
-
HDF5 documentation, NetCDF-Fortran documentation and PETSc: Fortran
-
Fortran Package Index and Fortran Programming Language: Best Practices, Arrays