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.

flowchart TD app["heatflow code and user code"] stdlib["stdlib_linalg: solve, chol, eigh, det, inv"] own["your own explicit interfaces to dgemm, dgesv, dpotrf"] lapack["LAPACK: dgesv, dpotrf, dsyev, dgeqrf, ..."] blas["BLAS: level 1, 2 and 3"] ref["reference BLAS and LAPACK from netlib"] openblas["OpenBLAS"] mkl["Intel oneMKL"] vendor["vendor libraries, for example Accelerate on macOS"] app --> stdlib app --> own stdlib --> lapack own --> lapack own --> blas lapack --> blas blas --- ref blas --- openblas blas --- mkl blas --- vendor

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

ddot dot product, daxpy y := alpha*x + y, dscal, dnrm2, dcopy, dswap, idamax

2

matrix and vector

O(n^2), memory bound

dgemv matrix-vector product, dtrsv triangular solve, dger rank-one update, dsymv

3

matrix and matrix

O(n^3) operations on O(n^2) data, so blocking can reuse every value many times

dgemm matrix product, dsyrk symmetric rank-k update, dtrsm triangular solve with many right-hand sides, dtrmm

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, c or z;

  • the next two letters are the matrix type;

  • the last three letters are the computation.

Matrix type Meaning Computation Meaning

ge

general (unsymmetric, possibly rectangular)

sv

simple driver: solve a linear system in one call

sy

symmetric

trf

factorize (triangular factorization)

po

symmetric or Hermitian positive definite

trs

solve using the factorization from trf

gt, pt

general tridiagonal, positive definite tridiagonal

ev

eigenvalues and eigenvectors

gb, pb

general band, positive definite band

svd

singular value decomposition

tr

triangular

qrf

QR factorization

or, un

orthogonal (real), unitary (complex)

ls

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.

How to read an interface:

  • Every scalar argument is passed by reference, as in all Fortran: a literal 1.0_dp for alpha is fine because it is intent(in).

  • character, intent(in) :: trans is 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 (sgemm and dgemm). A generic interface can group them under one name; stdlib does exactly that (stdlib_linalg).

  • intent is documentation that the compiler enforces on the Fortran side, not something the library knows: the declared intent(in) for a and intent(inout) for c match 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

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)
                                          ^

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 character dummy 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 dgemm and the other large routines on several threads by default. The environment variable OPENBLAS_NUM_THREADS sets 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

= 0

successful exit

< 0

the argument number -info had an illegal value: a programming error, such as lda < max(1, n), a negative size or a bad character code

> 0

the algorithm failed for a reason that depends on the data, and the meaning is routine specific: dgesv and dgetrf report the index i for which U(i, i) is exactly zero, dpotrf the order of a leading minor that is not positive definite, dsyev that an eigenvalue iteration did not converge

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

\[ A = L L^{T}, \]

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:

\[ L_{jj} = \sqrt{A_{jj} - \sum_{k=1}^{j-1} L_{jk}^{2}}, \qquad L_{ij} = \frac{1}{L_{jj}} \left( A_{ij} - \sum_{k=1}^{j-1} L_{ik} L_{jk} \right) \quad (i > j). \]

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 dpotrf was called once, not once per right-hand side. The usual pattern is dpotrf to factorize, then dpotrs for each new right-hand side: solving is O(n^2) once the factor exists.

  • dpotrf with uplo = '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. The uplo argument 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 dpotrf is 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 dptsv uses 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)

-llapack -lblas; LAPACK first, because it depends on BLAS

OpenBLAS

open source optimized BLAS that also carries a LAPACK, with multithreading and tuned kernels for many processors

-lopenblas provides both

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 -fcheck=all

external: one precision per routine, shapes and leading dimensions passed by hand

Result

a new array, written as an expression c = matmul(a, b)

written into c, with alpha, beta and transposition built in: C := alpha*op(A)*op(B) + beta*C

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, matmul(a(2:3, 2:3), b(2:3, 2:3))

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 libblas.a

0.088

0.519

OpenBLAS, 10 threads

0.087

0.051

OpenBLAS, OPENBLAS_NUM_THREADS=1

0.086

0.071

OpenBLAS with -fexternal-blas

0.033

0.021

reference BLAS with -fexternal-blas

0.418

0.381

LLVM Flang 23 -O2, OpenBLAS

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 err argument of type linalg_state_type. Without it, an error (a singular matrix, for instance) stops the program; with it, the program continues and can test err%error() and print err%print().

  • chol is the function and cholesky is the subroutine interface in this release; calling cholesky as a function gives the compile error “'cholesky' at (1) is not a function”.

  • The kind-generic BLAS names remove the d and s prefixes and the interface-writing: the next program calls gemm and dot with real64 and real32 arguments.

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 fftw3.f03, using iso_c_binding; documented in the FFTW manual

HDF5

self-describing hierarchical data files for large arrays

a Fortran API in the HDF5 distribution (module h5lt, hdf5, with h5open_f and so on)

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 external with implicit interfaces, which the example above shows is silently wrong on a kind mismatch; write explicit interfaces in a module, or use the stdlib modules

Types

Fortran 77 style declarations (double precision, real*8) and common workspaces; use real(dp) from iso_fortran_env, allocatable work arrays, and the workspace query

Intrinsics

hand-written triple loops for matrix products and ad hoc dot products; use matmul and dot_product, or dgemm when the problem is large

Build

link lines copied by hand; use fpm’s link key or CMake’s FindLAPACK and LAPACK::LAPACK

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; stdlib_linalg now provides a typed interface to the same routines

Standard version provenance and compiler support

Feature Defined in J3/23-007r1 Compiler support checked on 2026-10-19

matmul and dot_product intrinsics

16.9.134 and 16.9.73

GNU Fortran 16.2 and LLVM Flang 23 accept the examples

Interface bodies and import in an interface body

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 x(*) and sequence association

8.5.8.5 and 15.5.2.12

both

iso_c_binding and the FFTW Fortran 2003 header

18.2

both build and run the FFTW example

implicit none (type, external)

8.7

not used in the examples; -fimplicit-none enforces the type part

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

-fexternal-blas, -fblas-matmul-limit, -fexternal-blas64

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 use an 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 = 0 is the only success; check info < 0 as a bug and info > 0 as a data condition.

  • Believing info = 0 means an accurate answer. The singular 1..9 matrix passed. Compute a residual or a condition estimate.

  • Forgetting that the routines overwrite their inputs. a becomes the factors and b the solution; keep a copy (the residual check needs a0).

  • Mixing precisions. sgemm and dgemm are different routines; use real(dp) throughout and let the interface catch the rest.

  • Wrong link order. -llapack -lblas, not the other way round. -lopenblas alone is enough for OpenBLAS.

  • Not knowing which BLAS ran. Alternatives, LD_LIBRARY_PATH and a static libblas.a can each change it. Print the link line, or run ldd on the program, before timing it.

  • Thread oversubscription. Library threads times OpenMP threads times MPI ranks; set OPENBLAS_NUM_THREADS explicitly.

  • Timing one run. The threaded dgemm varied 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 matmul and dgemm results with ==. They differ in the last digits; compare with a tolerance.