Mastering C++ for scientific computing:
tools, tips, and tricks


Pieter Pas

Space:  Next slide
Shift+Space:  Previous slide
/:  Previous/next chapter
Esc:  Overview  F11:  Full screen

Outline


  1. Development tools
  2. Building an example project
  3. CMake concepts
  4. Package management
  5. Catching bugs early
  6. Best practices
  7. Optimization
  8. Interoperability with Python
  9. Useful resources

Development tools


Compilers


Compilers


Integrated development environments (IDEs)


  • Visual Studio (Windows only)
  • Xcode (macOS only)
  • Clion (€12/month, free student license)
  • Visual Studio Code
    • C/C++ extension
    • Clangd extension
    • CMake Tools extension
  • Other editors
    • Clangd language server

Integrated development environments (IDEs): Go to definition

Integrated development environments (IDEs): Refactoring

Integrated development environments (IDEs): Warnings and static analysis

Integrated development environments (IDEs): Debugging

Build system generators


  • Locate compiler and other tools
  • Detect compiler features
  • Allow configuration of project options
  • Locate dependencies
  • Propagate compiler and linker flags
  • Drive the build process
  • Install resulting binaries
  • Package resulting binaries
  • Drive test runners

Build system generators: example


Build system generators: example


Build system generators


  • CMake (CMakeLists.txt)
    • De facto standard
    • Huge ecosystem, loads of online resources
    • Compatible with all major IDEs and toolchains
    • Fast
    • Flexible
    • Interoperability with package managers
    • Supports testing and packaging
    • Easy to use for users of your project
    • Awkward syntax
    • Lots of poorly written legacy CMake code
    • If you are going to learn one build system, learn CMake

Build system generators


  • Meson (meson.build)
    • Declarative syntax
    • Intuitive command line interface
    • Fast
    • Gaining popularity
    • Not turing complete
    • Not turing complete
    • Smaller community compared to CMake
    • Requires Python

Build system generators


  • Bazel (BUILD)
    • Scalable to very large projects
    • Remote building, caching and testing
    • Arguably simpler syntax compared to CMake
    • Developed by Google for Google
    • Poor Windows support
    • Painful dependency management
    • Requires Java

Build system generators


  • SCons (SConstruct)
    • Extensible
    • Reproducible
    • Very slow
    • No package detection
    • Very poor/nonexistent cross-compilation support
    • Low-level, no transitive options
    • Lots of manual plumbing
    • Requires creating a custom, one-off build system
    • Requires Python

Build system generators


  • Xmake (xmake.lua)
    • Many features and many supported backends
    • Built-in package manager compatible with external repositories
    • Quite new
    • Mostly a single-developer effort
    • Most of the discussions and issues are in Chinese
    • “Everything but the kitchen sink”

Build system generators


  • GNU Autotools (autogen.sh, configure.sh)
    • Many older GNU/Linux projects use it
    • Outdated
    • Primitive and fragile dependency detection (manual or pkg-config)
    • Poor macOS and Windows compatibility
    • Very slow
    • Hard to understand, debug or patch

Build system generators


  • Make (Makefile)
    • Many older Linux projects use it
    • Available as CMake or Autotools backend
    • Very low-level build rules
    • Not a build system generator (no dependency or compiler detection etc.)
    • Cryptic syntax
    • Static recipes
    • Does not allow spaces in paths
    • Primitive timestamp-based checks
    • Manual dependency generation
    • Poor Windows compatibility
    • Not portable

Build system generators


  • Ninja (build.ninja)
    • A more modern Make alternative
    • Fast, easy multi-threading
    • Light-weight stand-alone binary
    • Support for C++ and Fortran modules
    • Excellent backend for CMake
    • Intended for code generation by a higher-level tool like CMake
    • Ideal CMake backend on Linux and macOS (use VS on Windows)

Building an example project


Example project



                        poly
                          ├── src                             -- library source code
                          │   ├── include                         -- public API header files
                          │   │   └── poly
                          │   │       ├── interpolate.hpp
                          │   │       └── poly.hpp
                          │   ├── src                             -- implementation files
                          │   │   └── interpolate.cpp
                          │   └── CMakeLists.txt                  -- library build script
                          ├── test                            -- unit tests
                          │   ├── CMakeLists.txt                  -- testing framework build script
                          │   └── test-interpolate.cpp
                          └── CMakeLists.txt                  -- user-facing build script

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt
Questions?

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project



                            poly
                              ├── src
                              │   ├── include
                              │   │   └── poly
                              │   │       ├── interpolate.hpp
                              │   │       └── poly.hpp
                              │   ├── src
                              │   │   └── interpolate.cpp
                              │   └── CMakeLists.txt
                              ├── test
                              │   ├── CMakeLists.txt
                              │   └── test-interpolate.cpp
                              └── CMakeLists.txt

Example project: build commands


  • Use Bash or Zsh shell on Linux/macOS, or in “Visual Studio Developer PowerShell” on Windows

                        # Configure
                        cmake -S . -B build -G "Ninja Multi-Config" -D POLY_INIT_NAN=On
                        # Build
                        cmake --build build --config Debug -j
                        # Run tests
                        cmake --build build --config Debug --target test

Example project: build commands



                        # Configure
                        cmake -S . -B build -G "Ninja Multi-Config" -D POLY_INIT_NAN=On
                        -- The CXX compiler identification is GNU 11.4.0
                        -- Detecting CXX compiler ABI info
                        -- Detecting CXX compiler ABI info - done
                        -- Check for working CXX compiler: /usr/bin/c++ - skipped
                        -- Detecting CXX compile features
                        -- Detecting CXX compile features - done
                        -- Found GTest: /usr/lib/x86_64-linux-gnu/cmake/GTest/GTestConfig.cmake (found version "1.11.0")  
                        -- Configuring done (0.5s)
                        -- Generating done (0.0s)
                        -- Build files have been written to: /home/pieter/GitHub/Cpp-Presentation/code/poly/build

                        # Build
                        cmake --build build --config Debug -j
                        [1/4] Building CXX object test/CMakeFiles/tests.dir/Debug/test-interpolate.cpp.o
                        [2/4] Building CXX object src/CMakeFiles/poly.dir/Debug/src/interpolate.cpp.o
                        [3/4] Linking CXX static library src/Debug/libpoly.a
                        [4/4] Linking CXX executable test/Debug/tests

                        # Run tests
                        cmake --build build --config Debug --target test
                        [0/1] Running tests...
                        Test project /home/pieter/GitHub/Cpp-Presentation/code/poly/build
                            Start 1: poly.interpolateCheby
                        1/1 Test #1: poly.interpolateCheby ............   Passed    0.00 sec
                        
                        100% tests passed, 0 tests failed out of 1
                        
                        Total Test time (real) =   0.00 sec

Example project: Visual Studio Code

Example project: Visual Studio Code

CMake concepts


CMake concepts


  1. Import and define targets
    • find_package(name)
    • add_library(target sources...)
    • add_executable(target sources...)
    • add_custom_target(target command args...)
  2. Set target properties and usage requirements
    • target_compile_features(target SCOPE features...)
    • target_compile_definitions(target SCOPE definitions...)
    • target_compile_options(target SCOPE options...)
    • target_include_directories(target SCOPE directories...)
    • target_link_options(target SCOPE options...)
    • set_target_properties(targets PROPERTIES property value)
  3. Express dependencies between targets
    • target_link_libraries(target SCOPE dependency)
  • Use targets and properties, do not set global flags

CMake concepts: scope and usage requirements


  • PUBLIC
    • Applies to current target and dependents (transitive)
  • PRIVATE
    • Applies to current target only
  • INTERFACE
    • Applies to dependents only (transitive)
PUBLIC PRIVATE INTERFACE
liba -flag -flag
libb -flag -flag
https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html#target-usage-requirements

CMake concepts: scope and usage requirements


  • Reconsider the poly example from earlier:

CMake concepts: scope and usage requirements


  • Which scope to use for compiler warnings?
    • Want to apply them to our own code ...
    • ... but not impose them on our users!
    • PRIVATE

                        # Interface libraries have no source files, only compiler flags etc.
                        add_library(warnings INTERFACE)
                        # Add compiler warnings for GCC
                        if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
                            target_compile_options(warnings INTERFACE "-Wall" "-Wextra" "-pedantic")
                        endif()

                        # Create an actual library target
                        add_library(poly "src/interpolate.cpp" ...)
                        # Apply warnings only when building poly, not when building dependents
                        target_link_libraries(poly PRIVATE warnings)
                    
                        # This could be a user program that uses the poly library
                        add_executable(user_program "main.cpp")
                        # Compiled without warnings, thanks to private link between poly and warnings
                        target_link_libraries(user_program PRIVATE poly)

CMake: further reading


Package management


Package management: where to get dependencies


  • System package manager (APT, Homebrew, winget)
    • Easy
    • Requires admin privileges
    • No control over package version
  • Build from source (manually using shell scripts)
    • Precise control over version and options
    • No admin privileges required
    • Does not scale well for large dependency trees
    • Slow
  • Include as Git submodule and CMake subdirectory (using add_subdirectory)
    • Dependencies are automatically built while building our own package
    • Similar drawbacks as above
  • C++ package manager (Vcpkg, Conan, Spack)
    • Simple declaration of required dependencies, features and versions
    • Good integration with CMake
    • Fast (binaries are often available)
    • Custom recipes, channels, repositories
    • Not all packages are available

Package management: Conan


  • List dependencies in conanfile.txt or conanfile.py file
  • Specify the CMakeDeps generator to make packages available using find_package
  • Specify the CMakeToolchain generator to pass options to CMake using a toolchain file
  • Choose the CMake layout for the build directories

                                poly
                                  ├── src
                                  │   ├── include
                                  │   │   └── poly
                                  │   │       ├── interpolate.hpp
                                  │   │       └── poly.hpp
                                  │   ├── src
                                  │   │   └── interpolate.cpp
                                  │   └── CMakeLists.txt
                                  ├── test
                                  │   ├── CMakeLists.txt
                                  │   └── test-interpolate.cpp
                                  ├── conanfile.txt
                                  └── CMakeLists.txt

                                [requires]
                                eigen/3.4.0
                                [test_requires]
                                gtest/1.12.1
                                [generators]
                                CMakeDeps
                                CMakeToolchain
                                [layout]
                                cmake_layout

Package management: Conan


  • Let Conan detect your system properties (only needed once)
    
                                    conan profile detect --force
  • Install dependencies of the poly project
    
                                    conan install . # Default generator and default build type
                                    conan install . \
                                        -c tools.cmake.cmaketoolchain:generator="Ninja" \
                                        -s build_type=Debug \
                                        --build=missing
                                
  • Generate, build and test the project using CMake
    
                                    cmake --preset conan-debug          # Configure
                                    cmake --build --preset conan-debug  # Build
                                    ctest --preset conan-debug          # Run tests
https://docs.conan.io/2/tutorial.html

Package management: Conan



                        cmake --preset conan-debug          # Configure
                        Preset CMake variables:
                        
                          CMAKE_BUILD_TYPE="Debug"
                          CMAKE_POLICY_DEFAULT_CMP0091="NEW"
                          CMAKE_TOOLCHAIN_FILE:FILEPATH="~/poly/build/Debug/generators/conan_toolchain.cmake"
                        
                        -- Using Conan toolchain: ~/poly/build/Debug/generators/conan_toolchain.cmake
                        -- Conan toolchain: C++ Standard 17 with extensions ON
                        -- The CXX compiler identification is GNU 11.4.0
                        -- Detecting CXX compiler ABI info
                        -- Detecting CXX compiler ABI info - done
                        -- Check for working CXX compiler: /usr/bin/c++ - skipped
                        -- Detecting CXX compile features
                        -- Detecting CXX compile features - done
                        -- Conan: Component target declared 'Eigen3::Eigen'
                        -- Conan: Component target declared 'GTest::gtest'
                        -- Conan: Component target declared 'GTest::gtest_main'
                        -- Conan: Component target declared 'GTest::gmock'
                        -- Conan: Component target declared 'GTest::gmock_main'
                        -- Conan: Target declared 'gtest::gtest'
                        -- Configuring done (0.3s)
                        -- Generating done (0.0s)
                        -- Build files have been written to: ~/poly/build/Debug

                        cmake --build --preset conan-debug  # Build
                        [1/4] Building CXX object test/CMakeFiles/tests.dir/Debug/test-interpolate.cpp.o
                        [2/4] Building CXX object src/CMakeFiles/poly.dir/Debug/src/interpolate.cpp.o
                        [3/4] Linking CXX static library src/Debug/libpoly.a
                        [4/4] Linking CXX executable test/Debug/tests

                        ctest --preset conan-debug          # Run tests
                        [0/1] Running tests...
                        Test project ~/poly/build
                            Start 1: poly.interpolateCheby
                        1/1 Test #1: poly.interpolateCheby ............   Passed    0.00 sec
                        
                        100% tests passed, 0 tests failed out of 1
                        
                        Total Test time (real) =   0.00 sec

Package management: alternatives


  • Conan (conan.io)
    • Cross-platform C/C++ package manager
    • Supports many different build systems (CMake, Meson, Visual Studio/MSBuild, Autotools ...)
    • Declarative conanfile.txt, flexible and extensible using Python scripting in conanfile.py
    • MIT license
  • Vcpkg (vcpkg.io)
    • Cross-platform C/C++ package manager
    • Supports CMake and MSBuild
    • Good integration with other Microsoft products like Visual Studio
    • Declarative vcpkg.json manifest file, CMake scripting in portfile.cmake
    • MIT license, opt-out telemetry
  • Spack (spack.io)
    • Linux and macOS package manager (no Windows support yet)
    • Aimed towards high-performance computing and supercomputing
    • Targets specific microarchitectures for optimal performance, many supported scientific languages, module support for managed HPC environments
    • MIT or Apache-2.0 license

Catching bugs early


Catching bugs early: compiler warnings


Catching bugs early: compiler warnings


  • Start project with high warning level
  • Enable warnings as errors for development builds
    • GCC, Clang: -Werror
    • MSVC: /WX
  • Enforce zero warning policy in continuous integration

Catching bugs early: compiler warnings


Catching bugs early: linting and static analysis


Catching bugs early: linting and static analysis


  • Clang-Tidy: configuration file
    • Like warnings, enable the majority of checks in the beginning
    • Remove checks for stylistic rules you don't follow (using minus sign)
    • Suppress false positives locally using NOLINT(name-of-check) comments
    • Disable checks that generate too many false positives
    • Don't forget HeaderFilterRegex so it also checks your header files!

                                poly
                                  ├── src
                                  │   ├── include
                                  │   │   └── poly
                                  │   │       ├── interpolate.hpp
                                  │   │       └── poly.hpp
                                  │   ├── src
                                  │   │   └── interpolate.cpp
                                  │   └── CMakeLists.txt
                                  ├── test
                                  │   ├── CMakeLists.txt
                                  │   └── test-interpolate.cpp
                                  ├── .clang-tidy
                                  ├── conanfile.txt
                                  └── CMakeLists.txt

                            ---
                            WarningsAsErrors: ''
                            HeaderFilterRegex: '(include/poly)'
                            FormatStyle: file
                            
                            Checks: |
                                *,
                                -abseil*,-altera*,-android*,-fuchsia*,-google*,-llvm*,-zircon*,
                                -modernize-use-trailing-return-type,
                                -*-magic-numbers,
                                google-build-using-namespace,
                            
                            CheckOptions:
                                - key: readability-implicit-bool-conversion.AllowPointerConditions
                                  value: true

Catching bugs early: linting and static analysis


Catching bugs early: Undefined Behavior


  • What is the result of compiling and running the following code?
    1. Compilation error
    2. Program runs but does not terminate
    3. Program runs but corrupts the hard drive
    4. Program runs and terminates immediately after printing Launching the nukes ...
    • Any of the above! (B. using GCC, D. using Clang)

Catching bugs early: Undefined Behavior


  • Undefined Behavior (UB) is a violation of language rules and compiler’s preconditions
    • No guarantees about the output of the compiler!
    • Runtime crash (e.g. segmentation fault) (best outcome)
    • Bug lies dormant, program appears to do what you expected (can change at any time!)
    • Program runs without crashing, silently generates incorrect results or has security vulnerability(worst outcome)
  • Many different sources:
    • Uninitialized variables
    • Array out of bounds
    • Signed integer overflow
    • Pointer casts
    • Data races
    • Null pointer dereference
    • Use after free
    • ...

Catching bugs early: preventing Undefined Behavior


  • How to prevent and detect Undefined Behavior?
    1. Awareness: know the rules of the language (en.cppreference.com)
    2. Follow good coding practices (isocpp.github.io/CppCoreGuidelines)
    3. Compiler warnings, linters and static analysis (previous slides)
    4. Run-time instrumentation and sanitizers (following slides)
    5. Run in specialized virtual machine like Valgrind (valgrind.org)

Catching bugs early: preventing Undefined Behavior


  • Address Sanitizer (-fsanitize=address)
    • Checks for memory errors (use after free, array out of bounds, dangling pointers)
  • Undefined Behavior Sanitizer (-fsanitize=undefined)
    • Checks for things like integer overflow, null pointer dereference, division by zero, pointer misalignment, invalid vpointers
  • Thread Sanitizer (-fsanitize=thread)
    • Checks for data races
    • Sanitizers do not catch 100% of errors!
https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html
https://clang.llvm.org/docs/AddressSanitizer.html
https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html
https://clang.llvm.org/docs/ThreadSanitizer.html
https://learn.microsoft.com/en-us/cpp/sanitizers/asan

Catching bugs early a bit later: debugging


  • Debuggers are extremely powerful tools
  • Many different front-ends and display modes (e.g GDB TUI layout)
  • Python scripting and pretty-printing (GDB Python API)
  • Conditional break-points
  • Hardware watchpoints to break when a memory location is accessed (GDB watch)
  • Remote debugging over SSH
  • Post-mortem analysis and reverse stepping (rr-project.org)
  • Instruction-level debugging and disassembly
  • ...

Best practices


Best practices: C++ Core Guidelines


Best practices: version control


  • Use Git for version control
    • KU Leuven has a GitLab instance (gitlab.kuleuven.be)
    • GitHub offers free Pro subscriptions for students (github.com)
    • Commit often
    • Pull and rebase often
    • Use meaningful commit messages
    • Ignore large files, caches, temporary build files etc. using .gitignore

Best practices: formatting


  • Clang-Format: automatic code formatter
    • Built-in IDE support
    • Always format before committing
    • Minimizes uninteresting changes in Git diffs

                                poly
                                  ├── src
                                  │   ├── include
                                  │   │   └── poly
                                  │   │       ├── interpolate.hpp
                                  │   │       └── poly.hpp
                                  │   ├── src
                                  │   │   └── interpolate.cpp
                                  │   └── CMakeLists.txt
                                  ├── test
                                  │   ├── CMakeLists.txt
                                  │   └── test-interpolate.cpp
                                  ├── .clang-format
                                  ├── .clang-tidy
                                  ├── conanfile.txt
                                  └── CMakeLists.txt

Best practices: documentation


  • Use a documentation generation tool
  • Document functions and variables using Doxygen-style docstrings (www.doxygen.nl/manual/docblocks.html)
  • Use comments to explain why a piece of code is necessary and how it achieves its task,
    do not simply repeat what the code does
  • Docstrings and API references are insufficient
    • Include high-level documentation that explains core concepts and overall architecture
  • Include examples

Best practices: continuous integration


Optimization


Optimization


  • “Premature optimization is the root of all evil”
    • Measure!
Premature optimization is the root of all evil, so to start this project I'd better come up with a system that can determine whether a possible optimization is premature or not.
(xkcd.com/1691)

Optimization


Optimization: performance tips


  • Build in Release mode (with optimizations enabled)
  • Compile for a modern CPU, e.g. -march=native, to get SSE/AVX support
  • Enable link-time optimization (LTO) (CMAKE_INTERPROCEDURAL_OPTIMIZATION=On)
  • Use profile-guided optimization (PGO)
  • Use vendor-optimized libraries for math, linear algebra, FFT, MPI, threading ...
  • If Debug mode is slow, use -Og instead of -O0
  • Beware of hidden costs (e.g. allocation) in tight loops
  • Design with cache locality and arithmetic intensity in mind
  • Eigen expression templates are faster than naive loops
  • Don't guess, measure!

Interoperability with Python


Interoperability with Python


  • pybind11 (github.com/pybind/pybind11)
  • nanobind (github.com/wjakob/nanobind)
  • Call fast C++ algorithms from Python
  • Call handy Python functions from C++
  • Expose C++ functions and classes to Python
  • Automatic conversions between NumPy arrays and Eigen matrices/vectors
  • For example:
    1. Load or generate data using pandas or scikit-learn
    2. Call fast C++ function that implements the main algorithm
    3. Process the results and plot using matplotlib
  • Create a Python package out of a CMake project using py-build-cmake (github.com/tttapa/py-build-cmake)

Interoperability with Python


Useful resources


Useful resources


Recap


Recap


  • Use a build system generator and package manager to make installation as frictionless as possible (e.g. CMake + Conan)
  • Beware of Undefined Behavior
  • Keep up with best practices (e.g. C++ Core Guidelines, conference talks)
  • Enable compiler warnings and static analysis (e.g. Clang-Tidy)
  • Use continuous integration for running tests (with sanitizers and Valgrind)
  • Include good documentation and examples
  • Don't get lost in micro-optimizations, measure real-world performance
https://github.com/tttapa/Mastering-Cpp-Scientific-Computing