Testing, Debugging and Profiling
|
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-18. Every
Fortran example was compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all (GNU Fortran
16.2) without warnings and run, and, where the feature is portable, also built and run with LLVM Flang 23. The tools were
run in a container (Debian, aarch64): fpm 0.13.0, test-drive 0.6.1, pFUnit 4.20.1, CMake 3.31.6 with CTest, gdb 16.3,
Valgrind 3.24.0, GNU gprof and gcov from GCC 16.2, and perf 6.12 (only the software cpu-clock event was available
inside the container). The output shown is real output. Two things were not run and are labelled as such wherever they
appear: Intel ifx (-check all, -qopt-report, VTune) and the Vegetables test framework. The standard says nothing
about any of the tools on this page, with one exception: it defines which conditions a program must not create (the
error and exception rules in
Error Handling and Program Termination),
and a runtime check, a sanitizer or a debugger is how you find out that you created one. Everything else here is
processor and tool dependent.
The examples use the heatflow project from the rest of the section (modules heatflow_kinds, heatflow_grid,
heatflow_kernels, heatflow_solver, and so on). Their sources, with the shell commands, are reproduced in full so that
a reader can follow along: a test suite in test/, small programs in example/, and the project built with
fpm or CMake. The sibling pages cover the surrounding topics:
Fortran Package Manager (fpm) and
Building with Make, CMake and Meson for the
build systems,
Compilers and Compiler Options for the compiler
flags, Common Gotchas and Pitfalls for the bugs
that these tools catch, and
Performance and Memory Layout for what to do with
a profile.
Which tool for which symptom
Fortran gives no exception, no stack trace by default and no unit test framework in the language. The tools are a compiler flag, a library or an external program, and each one finds a different kind of fault. Choose by symptom.
| Need | gfortran 16.2 | LLVM Flang 23 | Intel ifx (not run) |
|---|---|---|---|
Unit tests |
test-drive, pFUnit, Vegetables (compiler independent) |
test-drive passes through |
the frameworks are compiler independent |
Runtime checks |
|
no |
|
Uninitialised values |
|
no |
an |
Sanitizers |
|
|
not covered here |
Debugger |
gdb, with |
gdb, with |
gdb |
Call-count profiler |
|
|
see Intel’s documentation |
Sampling profiler |
perf, any build with |
perf, any build with |
VTune, perf |
Coverage |
|
no |
not covered here |
Optimization reports |
|
|
|
The Flang column is the result of trying each option with flang-23 in the container: a rejected option prints
error: unknown argument (for example flang-23: error: unknown argument: '-fcheck=bounds') and the compiler stops. The
ifx column only names options from Intel’s documentation, which was not run here (see the references).
Unit testing
A unit test calls one procedure with known inputs and compares the result with an answer worked out by hand. The test
passes or fails without anybody reading numbers, so it can run on every commit. For numerical code the comparison is
never == on reals: use a tolerance, absolute for values near zero and relative for large values, and choose it from the
algorithm’s expected accuracy rather than from machine epsilon.
Fortran has several community frameworks. None is part of the standard or of any compiler.
| Framework | Style | Notes |
|---|---|---|
plain Fortran, no preprocessor; a test is a subroutine with an allocatable |
a small library maintained under the fortran-lang GitHub organisation ( |
|
tests are written in |
supports MPI and OpenMP tests; richer assertions and fixtures; needs CMake and the preprocessor (run here: 4.20.1) |
|
Fortran sources processed by a generator; described in its repository; not checked here |
community project hosted on GitLab; not built or run for this page |
test-drive with fpm
fpm treats everything in test/ as tests. A test is an ordinary program, so a test-drive suite is a main program that
collects test suites (one module per unit under test) and runs them. The dependency is declared once:
[dev-dependencies]
test-drive = { git = "https://github.com/fortran-lang/test-drive", tag = "v0.6.1" }
and the project layout is
heatflow/
fpm.toml
src/ the library (heatflow_*.f90)
app/main.f90
test/
check.f90 the test runner (program)
test_grid.f90 suite 1: module test_grid, collect_grid
test_kernels.f90 suite 2: module test_kernels, collect_kernels
Source: fpm manifest reference
A suite is a module with a collect subroutine that returns an array of unittest_type values made by
new_unittest(name, procedure). Each test procedure has one dummy argument, type(error_type), allocatable,
intent(out) :: error; the test passes when error is left unallocated and fails when a check allocates it. The checks
are the generic check and the procedure test_failed; the check must be followed by if (allocated(error)) return
because the first failed check ends the test. This is the second suite of heatflow:
module test_kernels
use testdrive, only: new_unittest, unittest_type, error_type, check, skip_test, test_failed
use heatflow_kinds, only: dp
use heatflow_kernels, only: jacobi_sweep, ftcs_sweep
implicit none
private
public :: collect_kernels
contains
subroutine collect_kernels(testsuite)
type(unittest_type), allocatable, intent(out) :: testsuite(:)
testsuite = [ &
new_unittest('jacobi-fixed-point', test_jacobi_fixed_point), &
new_unittest('ftcs-linear-profile', test_ftcs_linear), &
new_unittest('ftcs-bad-expectation', test_bad_expectation, should_fail=.true.), &
new_unittest('gpu-only', test_skipped)]
end subroutine collect_kernels
!> A constant field is a fixed point of the Jacobi sweep: no change, zero residual.
subroutine test_jacobi_fixed_point(error)
type(error_type), allocatable, intent(out) :: error
real(dp) :: u(6, 6), unew(6, 6), res
u = 3.0_dp
unew = u
call jacobi_sweep(u, unew, res)
call check(error, res, 0.0_dp, thr=1.0e-14_dp)
if (allocated(error)) return
call check(error, all(unew == 3.0_dp), message='interior changed')
end subroutine test_jacobi_fixed_point
!> A linear temperature profile has zero second derivative, so FTCS leaves it unchanged.
subroutine test_ftcs_linear(error)
type(error_type), allocatable, intent(out) :: error
real(dp) :: u(7, 7), unew(7, 7), res
integer :: i
do i = 1, 7
u(i, :) = real(i, dp)
end do
unew = u
call ftcs_sweep(u, unew, 1.0_dp, 1.0e-3_dp, 0.1_dp, 0.1_dp, res)
call check(error, res, 0.0_dp, thr=1.0e-10_dp)
end subroutine test_ftcs_linear
!> Declared should_fail: the test passes when this check fails.
subroutine test_bad_expectation(error)
type(error_type), allocatable, intent(out) :: error
call check(error, 1 + 1, 3)
end subroutine test_bad_expectation
subroutine test_skipped(error)
type(error_type), allocatable, intent(out) :: error
call skip_test(error, 'needs a GPU')
end subroutine test_skipped
end module test_kernels
new_unittest takes an optional should_fail=.true., used above for a test that passes only if its check fails (it is
how a test of the test framework itself, or of an error path, is written), and skip_test marks a test as skipped with
a message. The runner is a program that lists the suites, runs them all, and turns the number of failures into a nonzero
exit status; the version below also reads the first two command-line arguments to select one suite and one test:
program check
use, intrinsic :: iso_fortran_env, only: error_unit
use testdrive, only: run_testsuite, run_selected, new_testsuite, testsuite_type, &
select_suite, get_argument
use test_grid, only: collect_grid
use test_kernels, only: collect_kernels
implicit none
integer :: stat, is
character(len=:), allocatable :: suite_name, test_name
type(testsuite_type), allocatable :: testsuites(:)
character(len=*), parameter :: fmt = '("#", *(1x, a))'
stat = 0
testsuites = [new_testsuite('grid', collect_grid), new_testsuite('kernels', collect_kernels)]
call get_argument(1, suite_name)
call get_argument(2, test_name)
if (allocated(suite_name)) then
is = select_suite(testsuites, suite_name)
if (is < 1 .or. is > size(testsuites)) then
write (error_unit, fmt) 'Available testsuites'
do is = 1, size(testsuites)
write (error_unit, fmt) '-', testsuites(is)%name
end do
error stop 1
end if
write (error_unit, fmt) 'Testing:', testsuites(is)%name
if (allocated(test_name)) then
call run_selected(testsuites(is)%collect, test_name, error_unit, stat)
if (stat < 0) error stop 1
else
call run_testsuite(testsuites(is)%collect, error_unit, stat)
end if
else
do is = 1, size(testsuites)
write (error_unit, fmt) 'Testing:', testsuites(is)%name
call run_testsuite(testsuites(is)%collect, error_unit, stat)
end do
end if
if (stat > 0) then
write (error_unit, '(i0, 1x, a)') stat, 'test(s) failed!'
error stop 1
end if
end program check
fpm test builds and runs it. Everything after -- goes to the test program, and --target selects one test executable
(globbing is supported):
fpm test
# Testing: grid
Starting spacing ... (1/2)
... spacing [PASSED]
Starting dirichlet ... (2/2)
... dirichlet [PASSED]
# Testing: kernels
Starting jacobi-fixed-point ... (1/4)
... jacobi-fixed-point [PASSED]
Starting ftcs-linear-profile ... (2/4)
... ftcs-linear-profile [PASSED]
Starting ftcs-bad-expectation ... (3/4)
... ftcs-bad-expectation [EXPECTED FAIL]
Message: Integer value mismatch
expected 3 but got 2
Starting gpu-only ... (4/4)
... gpu-only [SKIPPED]
Message: needs a GPU
fpm test -- kernels # one suite
fpm test -- kernels jacobi-fixed-point # one test of one suite
fpm test --target check -- grid # explicit test executable, then arguments
With kernels jacobi-fixed-point only ... jacobi-fixed-point [PASSED] is printed after the # Testing: kernels line,
and a suite name that does not exist prints the available suites and stops with ERROR STOP 1:
# Available testsuites
# - grid
# - kernels
ERROR STOP 1
Source: test-drive README and testdrive module and
fpm manifest reference (dev-dependencies, tests).
When a check fails, the message names the two values, and the run ends with a nonzero status that fpm reports. Changing
the expected cell spacing in test_grid.f90 from 0.1_dp to 0.2_dp gives:
# Testing: grid
Starting spacing ... (1/2)
... spacing [FAILED]
Message: Floating point value mismatch
expected 0.20000000000000001 but got 0.10000000000000001 (difference: 0.10000000000000001)
Starting dirichlet ... (2/2)
... dirichlet [PASSED]
# Testing: kernels
...
1 test(s) failed!
<ERROR> Execution for object " check " returned exit code 1
<ERROR> *cmd_run*:stopping due to failed executions
STOP 1
The fpm test exit status was 1. (The ERROR STOP 1 backtrace that gfortran prints before the suite output is
omitted, and the middle of the listing is shortened where it says ....)
The checks that check offers (from the test-drive README and the check generic interface in the source) are these.
Two overloads of real comparison exist, with thr= for an absolute tolerance and rel=.true. for a relative one.
| Call | Meaning |
|---|---|
|
true means pass |
|
zero means pass; use it for |
|
equal means pass |
|
|
|
fails with "Exceptional value 'not a number' found" when |
|
|
|
allocates |
The runner above does not use the extra features, but the library also writes JUnit XML (junit_output) and coloured
output (init_color_output), which its own tester uses and which continuous integration services read.
fpm builds every test with the flags of the selected profile, so a test run with fpm test --profile debug includes
-fcheck=bounds (see the runtime checks section) and is the cheapest extra safety net;
fpm test --flag "-fcheck=all -g -fbacktrace" adds all the checks to whatever fpm would use. With Flang the same suite
passes unchanged (fpm test --compiler flang-23): test-drive is standard Fortran. The output was identical to the
gfortran listing.
test-drive with CTest
Projects that build with CMake register each suite as a CTest test. test-drive publishes a CMake package, so
FetchContent can download it. Two details matter: the exported target is called test-drive (in version 0.6.1 the
ALIAS in its CMakeLists.txt is written test-drive:test-drive with a single colon, so linking to the namespaced
name test-drive::test-drive fails with "target was not found"), and test-drive adds its own self-tests to your CTest
list unless TESTDRIVE_BUILD_TESTING is switched off. This addition to heatflow’s CMakeLists.txt runs one CTest test
per suite, using the command-line selection built into the runner:
include(CTest)
if(BUILD_TESTING)
include(FetchContent)
FetchContent_Declare(test-drive
GIT_REPOSITORY https://github.com/fortran-lang/test-drive
GIT_TAG v0.6.1)
# keep test-drive's own self-tests out of our ctest run
set(TESTDRIVE_BUILD_TESTING OFF CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(test-drive)
add_executable(heatflow-tests test/check.f90 test/test_grid.f90 test/test_kernels.f90)
target_link_libraries(heatflow-tests PRIVATE heatflow test-drive)
add_test(NAME grid COMMAND heatflow-tests grid)
add_test(NAME kernels COMMAND heatflow-tests kernels)
endif()
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
cd build && ctest --output-on-failure
Test project /tmp/cb
Start 1: grid
1/2 Test #1: grid ............................. Passed 0.00 sec
Start 2: kernels
2/2 Test #2: kernels .......................... Passed 0.00 sec
100% tests passed, 0 tests failed out of 2
Total Test time (real) = 0.00 sec
ctest -R kernels -V shows the suite output, ctest --rerun-failed --output-on-failure repeats only the failures,
ctest -j4 runs tests in parallel, and ctest --output-junit results.xml writes a JUnit report (all four options
exist in CMake 3.31; the JUnit file was written and read here). CTest decides pass or fail from the exit status, so the
runner’s error stop 1 on failure is essential: a runner that only printed failures would be reported as passing.
Source: CTest manual (CMake 3.31).
pFUnit
pFUnit is built from source with CMake (no package manager is used here) and then found with find_package(PFUNIT).
The release built for this page was the v4.20.1 tag; the build took one cmake --build and the install tree contained
PFUNIT-4.20, GFTL-1.17, GFTL_SHARED-1.12 and FARGPARSE-1.12:
git clone --depth 1 https://github.com/Goddard-Fortran-Ecosystem/pFUnit.git
cmake -S pFUnit -B pfunit-build -G Ninja -DCMAKE_INSTALL_PREFIX="$HOME/pfunit" -DSKIP_MPI=YES -DSKIP_OPENMP=YES
cmake --build pfunit-build
cmake --install pfunit-build
A pFUnit test is a .pf file. Lines that begin with @ are directives for the preprocessor, which generates a driver
program and a Fortran module; the rest is ordinary Fortran. The assertion @assertEqual(expected, actual, tolerance=t)
works for scalars and arrays of the supported kinds, and @test(ifdef=NAME) compiles a test only when the macro is
defined (used below to switch a deliberately failing test on):
module test_kernels
use funit
use heatflow_kinds, only: dp
use heatflow_kernels, only: jacobi_sweep
implicit none
contains
@test
subroutine jacobi_fixed_point()
real(dp) :: u(6, 6), unew(6, 6), res
u = 3.0_dp
unew = u
call jacobi_sweep(u, unew, res)
@assertEqual(0.0_dp, res, tolerance=1.0e-14_dp)
@assertEqual(3.0_dp, unew, tolerance=1.0e-14_dp)
end subroutine jacobi_fixed_point
@test(ifdef=HEATFLOW_FAIL_DEMO)
subroutine deliberately_wrong()
@assertEqual(3, 1 + 1)
end subroutine deliberately_wrong
@test
subroutine jacobi_averages_neighbours()
real(dp) :: u(3, 3), unew(3, 3), res
u = 0.0_dp
u(1, 2) = 4.0_dp
unew = u
call jacobi_sweep(u, unew, res)
@assertEqual(1.0_dp, unew(2, 2), tolerance=1.0e-14_dp)
@assertEqual(1.0_dp, res, tolerance=1.0e-14_dp)
end subroutine jacobi_averages_neighbours
end module test_kernels
cmake_minimum_required(VERSION 3.20)
project(heatflow_pfunit LANGUAGES Fortran)
enable_testing()
find_package(PFUNIT REQUIRED)
file(GLOB HEATFLOW_SOURCES CONFIGURE_DEPENDS src/*.f90)
add_library(heatflow ${HEATFLOW_SOURCES})
set_target_properties(heatflow PROPERTIES Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/modules)
target_include_directories(heatflow PUBLIC ${CMAKE_BINARY_DIR}/modules)
add_pfunit_ctest(kernels_tests
TEST_SOURCES test/test_kernels.pf
LINK_LIBRARIES heatflow)
cmake -S . -B build -G Ninja -DCMAKE_PREFIX_PATH="$HOME/pfunit/PFUNIT-4.20"
cmake --build build
cd build && ctest --output-on-failure && ./kernels_tests
Test project /tmp/pb
Start 1: kernels_tests
1/1 Test #1: kernels_tests .................... Passed 0.00 sec
100% tests passed, 0 tests failed out of 1
Total Test time (real) = 0.00 sec
..
Time: 0.000 seconds
OK
(2 tests)
Configuring with -DCMAKE_Fortran_FLAGS=-DHEATFLOW_FAIL_DEMO compiles the third test, and the driver then reports the
assertion with its source line and a nonzero exit status (2):
..F.
Time: 0.000 seconds
Failure
in:
test_kernels_suite.deliberately_wrong
Location:
[test_kernels.pf:21]
AssertEqual failure:
Expected: (3)
Actual: (2)
Difference: <-1>
FAILURES!!!
Tests run: 3, Failures: 1, Errors: 0
, Disabled: 0
*** Encountered 1 or more failures/errors during testing ***
STOP 2
The generated driver accepts -h (usage), -d (verbose), -f (filter tests by pattern), -e (exclude), -o (output
file), -x (XML output), -t (TAP output) and --shuffle/--seed (random order), as printed by ./kernels_tests -h.
pFUnit also has MPI-parallel test cases and parameterized tests; those were not exercised here (the build used
-DSKIP_MPI=YES -DSKIP_OPENMP=YES). The trade-off against test-drive is plain: pFUnit has much richer assertions and
fixtures, but it adds a preprocessing step, a CMake-centred build and a longer installation, while test-drive is one
dev-dependency line.
Source: pFUnit repository and documentation.
Vegetables and others
Vegetables is a third community framework. It was not built or run for this page, so nothing is claimed here about its
syntax or commands; see its repository for the current instructions. Whichever framework you choose, the project structure is the same: the library in src/, the tests
in test/ calling only the public module interface, and an exit status that CI can read.
What to test
For a numerical code the useful tests are those that need no reference output: invariants (a constant field is a fixed
point of the Jacobi sweep; a linear profile has zero second difference, so the FTCS update leaves it alone: these are the
two tests above), conservation and symmetry properties, convergence order (halve the grid spacing and check that
the error falls by the expected factor), known analytic solutions (a single Fourier mode of the heat equation decays
as an exponential), boundary and degenerate cases (the smallest grid the code accepts, nx = 3), and error paths
(should_fail, or a status argument that must be nonzero; see
Error Handling and Program Termination).
Keep tests small and deterministic: seed random numbers, never depend on file paths or the clock, and make each test
independent of the order of the others.
Let the compiler find it first
The cheapest debugging happens before the program runs. gfortran reports many faults at compile time when the
information is available: with constant bounds, an index outside an array is a warning (Warning: Array reference at (1)
out of bounds (6 > 5) in loop beginning at (2)), a loop variable passed to an intent(inout) argument inside its own
loop is an error (Variable 'i' at (1) not definable inside loop beginning at (2) as INTENT(INOUT) argument to subroutine
'bump'), an array assignment between constant shapes that differ is an error (Different shape for array assignment at
(1) on dimension 1 (3 and 4)), and a recursive call of a procedure that is not recursive is an error (Function
'countdown' at (1) cannot be called recursively, as it is not RECURSIVE). Those four messages were produced by an
earlier, constant-size version of the program in the next section; the version printed below uses run-time sizes
precisely so that the compiler cannot see the fault.
The warning options worth turning on in development, each accepted by gfortran 16.2:
| Option | What it adds |
|---|---|
|
the baseline: unused variables, unused dummy arguments, possibly uninitialised variables (with optimization), truncated constants, and more |
|
rejects or warns about every extension; without it |
|
makes |
|
warns about variables read before assignment; it needs an optimization level ( |
|
implicit conversions that can change a value (for example |
|
calls to external procedures without an interface block or module, where the compiler cannot check arguments |
|
compile-time notice when an array temporary will be created for an argument |
|
assignments that may reallocate the left-hand side |
|
actual arguments that alias (the standard forbids this when one is modified) |
|
stop at the first error to avoid cascades |
LLVM Flang 23 reports semantic and conformance errors it finds and accepts -Wall, -Wextra and -pedantic, but it
does not accept -std=f2023 (error: Only -std=f2018 is allowed currently.; -std=f2018 is accepted) and its warning
set is smaller than gfortran’s. A build that must be portable should therefore also be compiled with gfortran under
the flags above in continuous integration. The full list of gfortran warnings is in its manual (Error and Warning Options).
Runtime checks
When the compiler cannot decide, it can insert tests that run with the program. They cost time (often a factor of two or more), so they belong in debug builds and in tests, not in production runs.
gfortran: -fcheck
-fcheck= takes a comma-separated list of keywords; a keyword prefixed with no- turns one off, so
-fcheck=all,no-array-temps is everything except the temporary-array warning. The keywords, from the manual, with a
description of what each tested here reports:
| Keyword | Checks |
|---|---|
|
every check below |
|
array subscripts against the declared bounds; for assumed-shape and deferred-shape arrays, against the actual bounds; array-shape conformance in assignments; substring ranges; the manual notes that some checks require the option when compiling the main program |
|
a run-time warning, once per location, when a temporary array had to be created for an actual argument (it points at copies that hurt performance) |
|
invalid arguments to bit intrinsics such as a shift larger than the bit size |
|
modification of a |
|
memory allocation; explicit |
|
pointers and allocatables: a disassociated pointer passed where a value is required, and so on |
|
a recursive call of a subroutine or function that is not marked |
-fcheck=none is not accepted (gfortran says Argument to '-fcheck' is not valid: none); omit the option to switch the
checks off. -fbounds-check is a deprecated alias for -fcheck=bounds. The program below has one deliberate fault per
keyword; the case number is the first command-line argument. The sizes are decided at run time so the compiler cannot
reject the code up front.
! One failing case per -fcheck= option; the case number is the first command-line argument.
module checks_cases
use, intrinsic :: iso_fortran_env, only: real64
implicit none
private
public :: run_case
integer, parameter :: dp = real64
abstract interface
subroutine walk_i(k)
integer, intent(in) :: k
end subroutine walk_i
end interface
procedure(walk_i), pointer :: hook => null()
contains
subroutine run_case(n)
integer, intent(in) :: n
select case (n)
case (1); call case_bounds()
case (2); call case_shape_mismatch()
case (3); call case_substring()
case (4); call case_pointer()
case (5); call case_do()
case (6); call case_recursion()
case (7); call case_array_temp()
case (8); call case_bits()
case default; print '(a)', 'no such case'
end select
end subroutine run_case
subroutine case_bounds()
real(dp), allocatable :: row(:)
integer :: i
allocate (row(5))
do i = 1, 6
row(i) = real(i, dp)
end do
print *, row(1)
end subroutine case_bounds
subroutine case_shape_mismatch()
real(dp), allocatable :: a(:), b(:)
allocate (a(3), b(4))
b = 1.0_dp
a(:) = b
print *, a(1)
end subroutine case_shape_mismatch
subroutine case_substring()
character(len=8) :: name
integer :: k
name = 'heatflow'
k = 9
print *, name(k:k)
end subroutine case_substring
subroutine case_pointer()
real(dp), pointer :: p
nullify (p)
call show(p)
end subroutine case_pointer
subroutine show(x)
real(dp), intent(in) :: x
print *, x
end subroutine show
subroutine case_do()
integer, target :: i
integer, pointer :: alias
alias => i
do i = 1, 3
call bump(alias)
end do
end subroutine case_do
subroutine bump(k)
integer, intent(inout) :: k
k = k + 1
end subroutine bump
subroutine case_recursion()
hook => walk
call hook(2)
end subroutine case_recursion
subroutine walk(k)
integer, intent(in) :: k
if (k > 0) call hook(k - 1)
end subroutine walk
subroutine case_array_temp()
real(dp) :: m(4, 4)
m = 1.0_dp
call total(m(2, :))
end subroutine case_array_temp
subroutine total(x)
real(dp), intent(in) :: x(4)
print *, sum(x)
end subroutine total
subroutine case_bits()
integer :: i, s
i = 1
s = 40
print *, shiftl(i, s)
end subroutine case_bits
end module checks_cases
program checks_demo
use checks_cases, only: run_case
implicit none
character(len=8) :: arg
integer :: n
call get_command_argument(1, arg)
read (arg, *) n
call run_case(n)
end program checks_demo
Compiled with gfortran -std=f2023 -Wall -Wextra -pedantic -fimplicit-none -fcheck=all -g -fno-backtrace
02_checks_catalogue.f90 (no warnings), the eight cases print:
$ ./a.out 1
At line 39 of file 02_checks_catalogue.f90
Fortran runtime error: Index '6' of dimension 1 of array 'row' above upper bound of 5
$ ./a.out 2
At line 48 of file 02_checks_catalogue.f90
Fortran runtime error: Array bound mismatch for dimension 1 of array 'a' (3/4)
$ ./a.out 3
At line 57 of file 02_checks_catalogue.f90
Fortran runtime error: Substring out of bounds: upper bound (9) of 'name' exceeds string length (8)
$ ./a.out 4
At line 63 of file 02_checks_catalogue.f90
Fortran runtime error: Pointer actual argument 'p' is not associated
$ ./a.out 5
At line 75 of file 02_checks_catalogue.f90
Fortran runtime error: Loop variable has been modified
$ ./a.out 6
At line 90 of file 02_checks_catalogue.f90
Fortran runtime error: Recursive call to nonrecursive procedure 'walk'
$ ./a.out 7
At line 98 of file 02_checks_catalogue.f90
Fortran runtime warning: An array temporary was created for argument 'x' of procedure 'total'
4.0000000000000000
$ ./a.out 8
At line 110 of file 02_checks_catalogue.f90
Fortran runtime error: SHIFT argument (40) out of range 0:32 in intrinsic SHIFTL
Every message names the source line, which is why -g is worth adding even though the line number appears without it.
Errors end the program with exit status 2; the warning in case 7 lets it continue. Without -fno-backtrace an address
backtrace (#0 0x... in ???, with source lines when -g is used) follows each error.
The same program built without any check (gfortran -O0 -g) behaves as follows, which is the reason the checks exist:
cases 1, 2 and 7 print their result as if nothing were wrong (the out-of-range write in case 1 and the shorter-array
assignment in case 2 silently succeed), case 3 prints a garbage character, case 4 dies with SIGSEGV, and cases 5, 6
and 8 run to the end (case 8 prints 0, because a shift by more than the bit size gives a processor-dependent value).
The same fault in heatflow shows the typical shape of a real bug. example/bounds_bug.f90 writes one cell too many into
the first row of a grid:
! An off-by-one in a boundary loop: writes one element past the end of a row.
program bounds_bug
use heatflow_kinds, only: dp
use heatflow_grid, only: grid_t, make_grid
implicit none
type(grid_t) :: g
integer :: i
g = make_grid(5, 5, 1.0_dp, 1.0_dp)
do i = 1, g%nx + 1
g%u(i, 1) = 0.0_dp
end do
print '(a,f6.2)', 'corner = ', g%u(1, 1)
end program bounds_bug
Built with fpm’s debug profile, which includes -fcheck=bounds, the error names the component and the line:
fpm run --example bounds_bug --profile debug
At line 11 of file example/bounds_bug.f90
Fortran runtime error: Index '6' of dimension 1 of array 'g%u' above upper bound of 5
Error termination. Backtrace:
#0 0xffff9ddd196f in ???
#1 0xffff9ddd26b3 in ???
#2 0xffff9ddd2c13 in ???
#3 0x400d63 in bounds_bug
at example/bounds_bug.f90:11
#4 0x400f7b in main
at example/bounds_bug.f90:3
<ERROR> Execution for object " bounds_bug " returned exit code 2
<ERROR> *cmd_run*:stopping due to failed executions
STOP 2
A plain fpm run --example bounds_bug printed corner = 0.00: fpm 0.13.0 passed no flags at all in that run (its
verbose output listed an empty COMPILER OPTIONS), so the error was not seen. With --profile debug fpm added
-Wall -Wextra -Wimplicit-interface -Wno-external-argument-mismatch -fPIC -fmax-errors=1 -g -fcheck=bounds
-fcheck=array-temps -fbacktrace -fcoarray=single, and --profile release added -O3 -Wimplicit-interface -fPIC
-fmax-errors=1 -funroll-loops -fcoarray=single. fpm run --flag "-fcheck=all" replaces the choice with exactly the
flags you list.
Other compilers
LLVM Flang 23 has no -fcheck option: flang-23 -fcheck=bounds stops with error: unknown argument: '-fcheck=bounds'.
It does check some things on its own. The same eight cases built with flang-23 -g gave: case 2 aborts with
fatal Fortran runtime error(...): Assign: mismatching element counts in array assignment (to 3, from 4) (exit status
134), case 4 is a segmentation fault (exit 139), and cases 1, 3, 5, 6, 7 and 8 run to the end without any message.
So a Flang build is not a debugging substitute for a gfortran -fcheck=all build; test with both.
Intel ifx has the -check option with an all keyword (see the -check page of Intel’s reference); it was not run for
this page, and its keyword set differs from gfortran’s. Check the vendor documentation for the release you use.
Debugging with gdb
gdb understands Fortran: it prints arrays with the Fortran subscript order, derived types with their components, shows
allocatable arrays by their current bounds, and accepts Fortran expressions (array sections, size(u, 1), % for
components). Build with -g and without optimization (-O0; at -O2 variables disappear into registers and lines
reorder). The demo is a short program that builds a 5 by 4 grid, sets a hot spot and calls one Jacobi sweep:
program gdb_demo
use heatflow_kinds, only: dp
use heatflow_grid, only: grid_t, make_grid
use heatflow_boundary, only: dirichlet
use heatflow_kernels, only: jacobi_sweep
implicit none
type(grid_t) :: g
real(dp), allocatable :: unew(:, :)
real(dp) :: residual
g = make_grid(5, 4, 1.0_dp, 1.0_dp)
call g%apply_boundary(dirichlet, 0.0_dp)
call g%set_hot_spot(100.0_dp)
unew = g%u
call jacobi_sweep(g%u, unew, residual)
print '(a,f8.3)', 'residual = ', residual
end program gdb_demo
A batch script (gdb reads commands from a file) shows the usual session. set disable-randomization off is only needed
inside a container that is not allowed to change the address-space layout; without it gdb prints a warning.
set pagination off
set disable-randomization off
break gdb_demo.f90:14
run
print g
ptype g
print g%u(3, 3)
print g%u(2:4, 2:3)
next
step
info args
print size(u, 1)
print u(2:4, 2:3)
next 3
info locals
watch residual
continue
continue
bt
delete
continue
fpm run --example gdb_demo --flag "-g -O0" --runner "gdb -q -batch -x gdb.cmds --args"
Breakpoint 1 at 0x400d50: file example/gdb_demo.f90, line 14.
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
Breakpoint 1, gdb_demo () at example/gdb_demo.f90:14
14 unew = g%u
$1 = ( nx = 5, ny = 4, dx = 0.25, dy = 0.33333333333333331, u = ((0, 0, 0, 0, 0) (0, 0, 0, 0, 0) (0, 0, 100, 0, 0) (0, 0, 0, 0, 0)) )
type = Type grid_t
integer(kind=4) :: nx
integer(kind=4) :: ny
real(kind=8) :: dx
real(kind=8) :: dy
real(kind=8), allocatable :: u(5,4)
End Type grid_t
$2 = 100
$3 = ((0, 0, 0) (0, 100, 0))
1 program gdb_demo
heatflow_kernels::jacobi_sweep (u=..., unew=..., residual=3.1151034035696666e-307) at ././src/heatflow_kernels.f90:11
11 pure subroutine jacobi_sweep(u, unew, residual)
u = ((0, 0, 0, 0, 0) (0, 0, 0, 0, 0) (0, 0, 100, 0, 0) (0, 0, 0, 0, 0))
unew = ((0, 0, 0, 0, 0) (0, 0, 0, 0, 0) (0, 0, 100, 0, 0) (0, 0, 0, 0, 0))
residual = 3.1151034035696666e-307
$4 = 5
$5 = ((0, 0, 0) (0, 100, 0))
18 do i = 2, size(u, 1) - 1
i = 65535
j = 2
Hardware watchpoint 2: residual
Hardware watchpoint 2: residual
Old value = 0
New value = 25
heatflow_kernels::jacobi_sweep (u=..., unew=..., residual=25) at ././src/heatflow_kernels.f90:18
18 do i = 2, size(u, 1) - 1
Hardware watchpoint 2: residual
Old value = 25
New value = 100
heatflow_kernels::jacobi_sweep (u=..., unew=..., residual=100) at ././src/heatflow_kernels.f90:18
18 do i = 2, size(u, 1) - 1
#0 heatflow_kernels::jacobi_sweep (u=..., unew=..., residual=100) at ././src/heatflow_kernels.f90:18
#1 0x00000000004010d4 in gdb_demo () at example/gdb_demo.f90:1
residual = 100.000
Things to read from this listing:
-
Array printing.
uhas 5 rows and 4 columns. gdb prints one parenthesised group per column, and inside a group the first subscript varies fastest, which is the column-major memory order. The hot spot100is in the third group (j = 3), third place (i = 3), andprint g%u(3, 3)confirms it.print g%u(2:4, 2:3)prints a section with the same convention: two groups of three. -
Derived types.
print gshows every component, including the allocatableu, andptype gshows the type with the current shapeu(5,4)of the allocatable component. -
Module procedures are named
module::procedure(heatflow_kernels::jacobi_sweep), andbreak heatflow_kernels::jacobi_sweepworks. -
Assumed-shape dummies (
u=...) are shown as...in frame lines;info argsorprint uexpands them. -
The first stop is before the prologue. The
pure subroutineline is reported with an unsetresidual(3.1151034035696666e-307is leftover stack) andi = 65535before the loop starts; real values appear after a fewnextcommands. Treat values printed at the very first line of a procedure as undefined. -
Watchpoints.
watch residualstops each time the variable changes and prints the old and new value: here the residual grows from 0 to 25 and then to 100. -
Quirks. The frame of the main program is reported at line 1 (
program gdb_demo) inbtand when stepping, not at the line of the call: the line table gfortran emits for the main program is coarse. The watchpoints in this session were hardware watchpoints; where the machine or a virtual environment cannot provide them gdb falls back to much slower software watchpoints.
A program built with Flang (fpm run --example gdb_demo --compiler flang-23 --flag "-g -O0" --runner ...) can be
debugged the same way. The only visible difference seen was the upper-case program name (GDB_DEMO () at
example/gdb_demo.f90:14) and identical print g and print g%u(2:4, 2:3) output. The step that follows the
assignment unew = g%u entered memcpy in Flang’s build instead of the Fortran call, which is a reminder that array
assignments can hide library calls.
Other useful gdb commands for Fortran programs: info locals, info frame, up/down to move through the stack,
tbreak for a one-time breakpoint, catch signal SIGFPE or simply run (a trapped floating-point exception stops
at the faulting line, see the next section), print $_siginfo after a signal, call to invoke a procedure, and
set var to change a value. Core dumps work the usual way (ulimit -c unlimited, then gdb ./a.out core). lldb was not tested here.
Source: GDB manual: Fortran.
Uninitialised data and signalling NaNs
Reading a variable before it is assigned is undefined behaviour that usually does not crash: the garbage often looks
like a plausible number, and the program gives the right answer on one machine and the wrong one on another. Fortran
has no automatic initialisation of local variables, and allocate does not zero memory. Two techniques make the
fault loud:
-
initialise every real to a signalling NaN and enable the invalid-operation trap, so the first use stops the program at the faulting line;
-
use a tool that tracks definedness (Valgrind with
--track-origins=yes, next sections).
gfortran provides the first technique with -finit-real=snan -ffpe-trap=invalid. The family of options (from the
manual) is -finit-local-zero, -finit-derived, -finit-integer=n, -finit-real=<zero|inf|-inf|nan|snan>,
-finit-logical=<true|false> and -finit-character=n. They do not initialise objects with the pointer attribute,
allocatable arrays or variables in an equivalence; and they silence -Wuninitialized for the variables they cover.
The manual also warns that compile-time optimization may turn a signalling NaN into a quiet one, so use these options
in -O0 builds.
For the objects the options do not cover, make the signalling NaN yourself with ieee_value from
ieee_arithmetic (ieee_signaling_nan is a standard ieee_class_type value, see
Floating-Point and IEEE Arithmetic). The
test program uses both:
! A work array whose halo is never initialised, caught with signalling NaNs.
program snan_demo
use, intrinsic :: iso_fortran_env, only: real64
use, intrinsic :: ieee_arithmetic, only: ieee_value, ieee_signaling_nan
implicit none
integer, parameter :: dp = real64
real(dp) :: local(4)
real(dp), allocatable :: work(:)
real(dp) :: poison, total
integer :: i, mode
character(len=8) :: arg
call get_command_argument(1, arg)
read (arg, *) mode
select case (mode)
case (1)
! uninitialised local: -finit-real=snan fills it
local(1:3) = 1.0_dp
total = sum(local)
print '(a,f8.3)', 'total = ', total
case (2)
! allocatable: -finit-real does NOT touch it, so poison it by hand
allocate (work(0:5))
poison = ieee_value(poison, ieee_signaling_nan)
work = poison
work(1:4) = 1.0_dp
total = 0.0_dp
do i = 0, 5
total = total + work(i)
end do
print '(a,f8.3)', 'total = ', total
end select
end program snan_demo
Built plainly, case 1 prints total = 3.000 (the fourth, unset element happened to be zero on this run) and case 2 prints
total = NaN (the NaN poison propagates by itself without any trap). Built with -finit-real=snan
-ffpe-trap=invalid, the first use of the unset value stops at the line that uses it:
case 1
Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.
Backtrace for this error:
#0 0xffffbb2e196f in ???
#1 0xffffbb2e05df in ???
#2 0xffffbb4af7ff in ???
#3 0x400d98 in snan_demo
at 04_snan.f90:20
Floating point exception
case 2
Program received signal SIGFPE: Floating-point exception - erroneous arithmetic operation.
Backtrace for this error:
#0 0xffffa8aa196f in ???
#1 0xffffa8aa05df in ???
#2 0xffffa8aa87ff in ???
#3 0x401058 in snan_demo
at 04_snan.f90:30
Floating point exception
Line 20 is total = sum(local) and line 30 is total = total + work(i). Run the same binary under gdb and the faulting statement and every variable are in front of you
(gdb -q -batch -ex run -ex bt -ex "info locals" --args ./a.out 2):
Program received signal SIGFPE, Arithmetic exception.
0x0000000000401058 in snan_demo () at 04_snan.f90:30
30 total = total + work(i)
#0 0x0000000000401058 in snan_demo () at 04_snan.f90:30
arg = '2 '
i = 0
local = (nan(0x4000000000000), nan(0x4000000000000), nan(0x4000000000000), nan(0x4000000000000))
mode = 2
poison = nan(0x4000000000000)
total = 0
work = (nan(0x4000000000000), 1, 1, 1, 1, nan(0x4000000000000))
dp = 8
The array work shows exactly which elements are poisoned (the two halo cells at 0 and 5), and i = 0 shows the loop was at its
first element. -finit-real=nan (quiet NaN) does not trap but makes the wrong answer obviously wrong (total = NaN).
The other IEEE traps, -ffpe-trap=zero,overflow,invalid, and the -ffpe-summary option that prints which flags were
raised at exit, are described in
Error Handling and Program Termination.
LLVM Flang 23 rejects -finit-real and -ffpe-trap; its build of the same program printed total = 3.000 for case 1
(again by luck) and NaN for case 2, because only the explicit ieee_value poisoning is portable. Intel’s ifx has an
-init option for this purpose (not run here).
Sanitizers
-fcheck only knows about the arrays the compiler can see. Once an array is passed to an explicit-shape dummy, to
C, or through a pointer, its extent is gone and -fcheck is silent. The sanitizers instrument every memory access of
the compiled code and keep a shadow map of valid memory. gfortran supports the GCC sanitizers
(-fsanitize=address, undefined, leak, thread, among others, from GCC’s instrumentation options); combine them
with -g and -fno-omit-frame-pointer for readable stacks. This program holds the faults that -fcheck misses:
! Memory bugs that -fcheck=all does not see once the compiler has lost the array bounds.
module memory_bugs
use, intrinsic :: iso_fortran_env, only: real64, int32
implicit none
private
public :: run_case
integer, parameter :: dp = real64
contains
subroutine run_case(n)
integer, intent(in) :: n
select case (n)
case (1); call heap_overflow()
case (2); call use_after_free()
case (3); call leak()
case (4); call int_overflow()
case (5); call uninitialized()
case default; print '(a)', 'no such case'
end select
end subroutine run_case
!> Writes past the end of a heap array through an explicit-shape dummy.
subroutine heap_overflow()
real(dp), allocatable :: u(:)
allocate (u(10))
u = 0.0_dp
call fill(u, 11)
print *, u(1)
end subroutine heap_overflow
subroutine fill(x, n)
integer, intent(in) :: n
real(dp), intent(inout) :: x(n)
x(n) = 1.0_dp
end subroutine fill
!> Reads through a pointer after the target was deallocated.
subroutine use_after_free()
real(dp), pointer :: p(:), q(:)
real(dp) :: v
allocate (p(10))
p = 2.0_dp
q => p
deallocate (p)
v = q(3)
print *, v
end subroutine use_after_free
!> The pointer goes out of scope without a deallocate.
subroutine leak()
real(dp), pointer :: work(:)
allocate (work(1000))
work = 1.0_dp
print *, sum(work)
end subroutine leak
subroutine int_overflow()
integer(int32) :: k
integer :: i
k = huge(k) - 2_int32
do i = 1, 5
k = k + 1_int32
end do
print *, k
end subroutine int_overflow
!> A local that is read before it is ever assigned.
subroutine uninitialized()
real(dp) :: total, x(4)
integer :: i
x = [1.0_dp, 2.0_dp, 3.0_dp, 4.0_dp]
do i = 1, 4
total = total + x(i)
end do
if (total > 5.0_dp) print *, 'big'
end subroutine uninitialized
end module memory_bugs
program memory_bugs_demo
use memory_bugs, only: run_case
implicit none
character(len=8) :: arg
integer :: n
call get_command_argument(1, arg)
read (arg, *) n
call run_case(n)
end program memory_bugs_demo
The five cases built plainly (gfortran -g -O0) all appear to work: case 1 prints 0.0000000000000000, case 2
2.0000000000000000, case 3 1000.0000000000000, case 4 -2147483646 and case 5 big. Built with
gfortran -g -O0 -fsanitize=address,undefined -fno-omit-frame-pointer:
==29==ERROR: AddressSanitizer: heap-buffer-overflow on address 0xfc6f9f7e0070 at pc 0x000000402e04 bp 0xffffea92e9a0 sp 0xffffea92e9b8
WRITE of size 8 at 0xfc6f9f7e0070 thread T0
#0 0x000000402e00 in __memory_bugs_MOD_fill 03_memory_bugs.f90:35
#1 0x000000403160 in __memory_bugs_MOD_heap_overflow 03_memory_bugs.f90:28
#2 0x0000004035d8 in __memory_bugs_MOD_run_case 03_memory_bugs.f90:14
#3 0x000000403b88 in memory_bugs_demo 03_memory_bugs.f90:85
...
0xfc6f9f7e0070 is located 0 bytes after 80-byte region [0xfc6f9f7e0020,0xfc6f9f7e0070)
allocated by thread T0 here:
#0 0xffffacfd4428 in malloc (/usr/local/lib64/libasan.so.8+0xd4428)
#1 0x000000403068 in __memory_bugs_MOD_heap_overflow 03_memory_bugs.f90:26
...
SUMMARY: AddressSanitizer: heap-buffer-overflow 03_memory_bugs.f90:35 in __memory_bugs_MOD_fill
(Case 1.) The report names the exact line of the write (35, in fill), the call chain, and where the 80-byte block
was allocated (line 26). Case 2, the read of freed memory, is reported as heap-use-after-free with three stacks:
the read (line 46), the deallocate that freed it (line 45) and the original allocate (line 42). Case 3, the
forgotten deallocate, is found by the leak checker when the program ends (the output shortened):
=================================================================
==35==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 8000 byte(s) in 1 object(s) allocated from:
#0 0xffff8c5c4428 in malloc (/usr/local/lib64/libasan.so.8+0xd4428)
#1 0x000000401d74 in __memory_bugs_MOD_leak 03_memory_bugs.f90:51
...
and the exit status of the program changes to 1, which makes leaks fail a CI job. Case 4 is reported by the undefined behaviour sanitizer, and the run continues:
03_memory_bugs.f90:61:21: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'integer(kind=4)'
-2147483646
Case 5 (an uninitialised local) is not found by AddressSanitizer: it checks addresses, not definedness. Use
-finit-real=snan or Valgrind for that.
Notes:
-
Library calls are not instrumented. In an earlier version of the program the read in case 2 was written
print *, q(3); AddressSanitizer did not report it, because the element is read inside the (uninstrumented) Fortran runtime library, not by compiled code. Copy the value to a local first (v = q(3)), as the listing does, when you test this kind of fault. -
Leaks in the main program. Run through fpm (
fpm test --flag "-g -O0 -fsanitize=address,undefined -fno-omit-frame-pointer" --link-flag "-fsanitize=address,undefined"), the test suite of this page passed all its tests and then LeakSanitizer ended the run with exit status 1 andSUMMARY: AddressSanitizer: 59 byte(s) leaked in 3 allocation(s), allocated attest/check.f90:14, thetestsuites = [...]assignment in the runner. A variable declared in a main program has thesaveattribute (J3/23-007r1 8.5.16), so gfortran does not deallocate it atend program. Addingdeallocate (testsuites)before the final check removed the report. Such leaks are harmless, but they make a leak-checked CI job fail; free the data or move it into a procedure. -
Compile every file with the flag and link with it. A sanitizer-compiled object mixed with plain ones sees only part of the program. (The fpm command is above.)
-
Do not combine
-fsanitize=addresswith-fsanitize=thread, and expect runs several times slower and memory use two to three times larger. -
Flang.
flang-23 -fsanitize=addressstops witherror: unknown argument: '-fsanitize=address'; so doundefinedandthread. Flang-built binaries can still be run under Valgrind (below) and perf.
Valgrind
Valgrind runs an unmodified binary on a virtual CPU that checks every memory access and tracks which bits of every
value are defined. It needs no special compile flag except -g for line numbers, is slower (typically 10 to 50 times),
and finds what AddressSanitizer finds plus uninitialised uses. On the same program, built with plain gfortran -g -O0
and run as valgrind --leak-check=full --track-origins=yes -q ./a.out N:
==13== Invalid write of size 8
==13== at 0x4011CC: __memory_bugs_MOD_fill (03_memory_bugs.f90:35)
==13== by 0x401317: __memory_bugs_MOD_heap_overflow (03_memory_bugs.f90:28)
==13== by 0x4013F3: __memory_bugs_MOD_run_case (03_memory_bugs.f90:14)
==13== by 0x401517: MAIN__ (03_memory_bugs.f90:87)
==13== by 0x401557: main (03_memory_bugs.f90:81)
==13== Address 0x4d311a0 is 0 bytes after a block of size 80 alloc'd
==13== at 0x488545C: malloc (vg_replace_malloc.c:446)
==13== by 0x40127B: __memory_bugs_MOD_heap_overflow (03_memory_bugs.f90:26)
for case 1 (Invalid write of size 8). Case 2 gives Invalid read of size 8 with Address 0x4d31160 is 16 bytes inside
a block of size 80 free'd with the stack of the free and of the malloc. Case 3 gives, after the program’s own output:
==15== 8,000 bytes in 1 blocks are definitely lost in loss record 1 of 1
==15== at 0x488545C: malloc (vg_replace_malloc.c:446)
==15== by 0x400E3F: __memory_bugs_MOD_leak (03_memory_bugs.f90:53)
and case 5, the read of the uninitialised accumulator, which neither -fcheck nor AddressSanitizer saw:
==17== Conditional jump or move depends on uninitialised value(s)
==17== at 0x400CC8: __memory_bugs_MOD_uninitialized (03_memory_bugs.f90:76)
==17== by 0x401413: __memory_bugs_MOD_run_case (03_memory_bugs.f90:18)
==17== by 0x401517: MAIN__ (03_memory_bugs.f90:87)
==17== by 0x401557: main (03_memory_bugs.f90:81)
==17== Uninitialised value was created by a stack allocation
==17== at 0x400C44: __memory_bugs_MOD_uninitialized (03_memory_bugs.f90:69)
The report points at the first use that changes control flow (the if), and --track-origins=yes says where the value
came from (the stack frame of uninitialized, which is where total lives). Valgrind does not report an uninitialised
value that is only copied or added, until it is tested, printed or used in a system call. Case 4 (integer overflow)
produces no report, because the processor does not treat it as a fault. Use -q to drop the banner, --error-exitcode=1
to turn errors into a failing exit status in CI, and --suppressions=file for known library noise. Valgrind works on
binaries from any compiler; on this aarch64 container it was run on gfortran-built executables only.
Source: Valgrind User Manual.
Profiling
A profile answers "where does the time go?" Measure the optimized program, not the debug build: bounds checks and
-O0 change the balance. Use realistic sizes, keep -g for line information (it does not slow the code), and run
long enough to collect samples. The demonstration program is a 500 by 500 FTCS run of 400 steps with a deliberately
badly ordered diagnostic added; energy_by_rows sums the grid with the first subscript in the outer loop, so it strides
through memory:
! A heatflow run with a deliberately slow diagnostic, used for profiling.
program profile_demo
use heatflow_kinds, only: dp
use heatflow_grid, only: grid_t, make_grid
use heatflow_boundary, only: dirichlet
use heatflow_solver, only: solver_t, make_solver
implicit none
type(grid_t) :: g
class(solver_t), allocatable :: s
real(dp) :: residual, total
integer :: k
g = make_grid(500, 500, 1.0_dp, 1.0_dp)
call g%apply_boundary(dirichlet, 0.0_dp)
call g%set_hot_spot(100.0_dp)
s = make_solver('ftcs', g, dirichlet, 0.0_dp, 1.0_dp, 1.0e-6_dp)
total = 0.0_dp
do k = 1, 400
call s%step(residual)
total = total + energy_by_rows(s%grid%u)
end do
print '(a,es10.3,a,f12.3)', 'residual = ', residual, ' mean energy = ', total/400.0_dp
contains
!> Sums u with the first index in the outer loop: strides through memory.
function energy_by_rows(u) result(e)
real(dp), intent(in) :: u(:, :)
real(dp) :: e
integer :: i, j
e = 0.0_dp
do i = 1, size(u, 1)
do j = 1, size(u, 2)
e = e + u(i, j)
end do
end do
end function energy_by_rows
end program profile_demo
Source: GNU gprof manual
gprof
gprof is the classic instrumenting profiler: -pg makes the compiler insert a call counter at each procedure entry and a
sampling timer, and the program writes gmon.out into the current directory when it ends. Both compile and link need the
flag; -fno-inline keeps small procedures visible as separate entries.
fpm run --example profile_demo --flag "-O2 -g -pg -fno-inline" --link-flag "-pg"
gprof -b -p build/gfortran_*/example/profile_demo gmon.out | head
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
58.86 0.10 0.10 400 0.25 0.25 __heatflow_kernels_MOD_ftcs_sweep
23.54 0.14 0.04 400 0.10 0.10 energy_by_rows.0
17.66 0.17 0.03 400 0.08 0.33 __heatflow_solver_MOD_ftcs_step
0.00 0.17 0.00 401 0.00 0.00 __heatflow_grid_MOD_grid_apply_boundary
(The glob in the second command assumes one fpm build directory; fpm names it with a hash.) The flat profile ranks
procedures by self time: the kernel is the biggest, but the diagnostic takes a quarter of the run. -q prints the
call graph, in which MAIN__ calls ftcs_step 400 times with children time 0.10 and energy_by_rows 400 times.
Module procedures appear with their mangled names (__module_MOD_name). Limits: gprof’s sample interval is 10 ms, so
short runs are noisy (this one lasted 0.17 s; read the percentages as rough); inlined procedures do not appear
(hence -fno-inline, which changes the optimization); and multi-threaded programs are not profiled correctly. Flang
rejects -pg.
perf
perf samples the program from outside, using the hardware performance counters or a software timer, with no
instrumentation and no recompilation beyond -g. Frame pointers (-fno-omit-frame-pointer) make call graphs
(perf record -g) reliable. On a normal Linux host the commands would be:
fpm run --example profile_demo --flag "-O2 -g -fno-omit-frame-pointer"
perf record -g build/gfortran_*/example/profile_demo
perf report --stdio --no-children
The container used for this page does not allow the default hardware cycles event (No permission to enable cycles:Pu
event), so the run used the software clock and extra capabilities (docker run --cap-add SYS_ADMIN --cap-add PERFMON
--security-opt seccomp=unconfined), with perf record -e cpu-clock -F 999 (without -g, because call-graph unwinding printed unwind: get_proc_name unsupported in this container). The report listed the same two procedures:
46.01% profile_demo profile_demo [.] __heatflow_kernels_MOD_ftcs_sweep
39.88% profile_demo profile_demo [.] energy_by_rows.0
perf stat summarises a run (166.17 msec cpu-clock, 0.970 CPUs utilized here); on hardware with counters,
perf stat -e cycles,instructions,cache-misses shows instructions per cycle and cache behaviour, which is what tells
the strided energy_by_rows from the contiguous kernel. perf was run on gfortran builds only; because it needs only
symbols, it works on Flang and ifx binaries too. The sampled shares differ from gprof’s (46 and 40 against 59 and
24) because of the low sample count and because -fno-inline was used for gprof only: both numbers are samples, not
measurements. Fixing energy_by_rows (loop over j outside, i inside, or sum(u)) is covered in
Performance and Memory Layout.
Intel VTune
Intel VTune Profiler is Intel’s sampling profiler with hardware-event analyses (hotspots, memory access, threading). It was not installed or run for this page and no command is shown; see Intel’s documentation.
Coverage with gcov
gcov reports which lines a test run executed, which answers the question "what do the tests not test?". Compile and link
with --coverage (which means -fprofile-arcs -ftest-coverage at compile time and -lgcov at link time), run the
tests, then run gcov on the data files:
fpm test --flag "--coverage -O0" --link-flag "--coverage"
gcov build/gfortran_*/heatflow/src_heatflow_grid.f90.gcda
File 'src/heatflow_grid.f90'
Lines executed:43.59% of 39
Creating 'heatflow_grid.f90.gcov'
The .gcov file is the source annotated with execution counts, and ##### marks lines that never ran:
2: 42: select case (bc)
-: 43: case (dirichlet)
6: 44: self%u(1, :) = value
6: 45: self%u(self%nx, :) = value
6: 46: self%u(:, 1) = value
6*: 47: self%u(:, self%ny) = value
-: 48: case (neumann)
#####: 49: self%u(1, :) = self%u(2, :)
#####: 50: self%u(self%nx, :) = self%u(self%nx - 1, :)
#####: 51: self%u(:, 1) = self%u(:, 2)
#####: 52: self%u(:, self%ny) = self%u(:, self%ny - 1)
#####: 54: self%u(1, :) = self%u(self%nx - 1, :)
The Neumann and periodic branches of apply_boundary are never exercised by the suite, which is a concrete next test to
write. The same run reported 100% of the 14 lines of heatflow_kernels.f90. The counts on the Dirichlet lines are
per-line execution counts of the compiler’s generated loop code, not the number of calls (the function was called once);
read the zero versus nonzero distinction, not the numbers. gcov -b adds branch probabilities. Other front ends exist
for .gcda data (HTML reports from lcov or gcovr); they were not installed here. Flang lists -fprofile-generate and -fprofile-use for LLVM
profile data, but the llvm-cov and llvm-profdata tools were not present in the container, so Flang coverage was
not tested.
Source: GCC manual: gcov and GNU gprof manual.
Compiler optimization reports
An optimization report asks the compiler what it did to a loop. For performance work this is often faster than
profiling: it tells you directly that a loop was or was not vectorized, and why. gfortran uses -fopt-info, with
a pass filter (vec, loop, inline, or all), a verbosity (optimized, missed, note, or all) and an optional
output file (-fopt-info-vec=vec.txt). On the heatflow kernels at -O3:
gfortran -O3 -fopt-info-vec-optimized -c heatflow_kernels.f90
heatflow_kernels.f90:34:12: optimized: loop vectorized using 16 byte vectors and unroll factor 2
heatflow_kernels.f90:34:12: optimized: loop vectorized using 16 byte vectors and unroll factor 2
heatflow_kernels.f90:18:12: optimized: loop vectorized using 16 byte vectors and unroll factor 2
and with -fopt-info-vec-missed the loops that were not:
heatflow_kernels.f90:33:10: missed: couldn't vectorize loop
heatflow_kernels.f90:37:72: missed: not vectorized: complicated access pattern.
heatflow_kernels.f90:33:10: missed: couldn't vectorize loop
heatflow_kernels.f90:37:72: missed: not vectorized: not suitable for strided load _22 = *_21;
heatflow_kernels.f90:17:10: missed: couldn't vectorize loop
heatflow_kernels.f90:19:84: missed: not vectorized: complicated access pattern.
Lines 17 and 33 are the outer do j loops, 18 and 34 the inner do i loops. gfortran vectorized the inner loops of
both sweeps (the report mentions lines 34 and 18, a loop appearing twice when the compiler made two versions of it) and
declined the outer loops, which is the usual and desirable outcome: one level of vectorization, along the contiguous
first subscript. The missed lines are the reasons for the outer loops. Adding the contiguous attribute to the two
assumed-shape dummies did not change which loops were vectorized in this experiment. See
Performance and Memory Layout for what to do
when an inner loop is the one that is missed.
LLVM Flang uses the LLVM remark options -Rpass=<regex>, -Rpass-missed=<regex> and -Rpass-analysis=<regex> (the driver
lists all three). The same kernel file compiled with flang-23 -O3 -Rpass-missed=loop-vectorize printed
heatflow_kernels.f90:18:7: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
heatflow_kernels.f90:34:7: remark: loop not vectorized [-Rpass-missed=loop-vectorize]
(each remark appears twice; paths were shortened) and -Rpass-analysis=loop-vectorize gives the reason: loop not
vectorized: value that could not be identified as reduction is used outside the loop. With -Rpass=loop-vectorize
nothing was printed, because no loop was vectorized. The remarks refer to the inner loops (lines 18 and 34), which
update residual with max and write unew, so on this kernel the two compilers differ at -O3: gfortran vectorized
the inner loops and Flang 23 did not. That is worth knowing before comparing their speeds.
Intel ifx writes a report with -qopt-report (see the -qopt-report page of Intel’s reference); that option and its
levels were not run here.
Where older books and tutorials are outdated
-
Test frameworks. Older texts recommend FUnit (the Ruby-based predecessor of pFUnit) or hand-written
if (abs(x - y) > tol) stopchecks. The maintained frameworks today are pFUnit 4 (rewritten, CMake, preprocessor) and the fortran-lang test-drive; a build system test (fpm test, CTest) replaces the shell-script runners. -
Runtime checks. Old compilers called it
-Cor-check bounds; gfortran spells it-fcheck=with keyword lists,-fbounds-checkis a deprecated alias, and the list has grown (do,mem,pointer,recursion,bits,array-temps). -
Recursion. Older books say a procedure must be declared
recursiveto call itself. Since Fortran 2018 a procedure is recursive by default (therecursivekeyword is advisory) andnon_recursivestates the opposite (J3/23-007r1 15.6.2.1 and Annex C.1). gfortran 16.2 still diagnoses recursion in a procedure that is not declaredrecursive, with an error at compile time or the-fcheck=recursionerror at run time, so writerecursivefor portability across compilers. -
Sanitizers. The AddressSanitizer and UndefinedBehaviorSanitizer family was added to GCC after most Fortran books were written, and works with gfortran without Fortran-specific support, but only reports what is visible to compiled code (not library-internal reads and not uninitialised values).
-
Profiling. gprof and
-pgare all that older books mention; sampling profilers (perf, VTune) need no recompilation and work with optimized code, and optimization reports (-fopt-info,-Rpass,-qopt-report) are the modern first step for vectorization questions. -
Debuggers. Debugger-specific sections in old books describe vendor debuggers; gdb prints Fortran arrays, sections and derived types directly.
See also Documentation and Editor Tooling for the language server and formatter, and Best Practices and Style for how tests and checks fit into project practice.
References
-
test-drive repository (README,
testdrivemodule, CMake and Meson packaging) -
GNU Fortran manual: Options for code generation conventions (-fcheck, -finit-real, -fstack-arrays) and Options for debugging your program (-ffpe-trap, -ffpe-summary) and Options to request or suppress errors and warnings
-
GCC manual: Program instrumentation options (-fsanitize, -pg, --coverage) and Developer options (-fopt-info)
-
LLVM Flang documentation and Clang user manual: optimization remarks (-Rpass)
-
Intel Fortran Compiler reference: qopt-report (also the
checkandinitpages of the same reference); not run for this page