Struggling with Parallel Programming? CMake Provides a One-Click Solution!

Struggling with Parallel Programming? CMake Provides a One-Click Solution!Struggling with Parallel Programming? CMake Provides a One-Click Solution!Click the blue text to follow the author

1. Introduction

In the field of high-performance computing, parallel programming is crucial for fully leveraging the performance potential of modern computer hardware, especially multi-core processors and distributed computing clusters. OpenMP and MPI are two widely used parallel programming models, suitable for shared memory and distributed memory systems, respectively. OpenMP allows programmers to parallelize serial code through simple compilation directives (pragma), while MPI enables communication and cooperation between processes through a message-passing mechanism.

CMake is a powerful cross-platform build system generator that simplifies the configuration, compilation, and linking processes of projects. For parallel programs involving OpenMP and MPI, correctly configuring CMake is essential to ensure that the code can be compiled, linked, and executed correctly.

This article details how to detect and configure OpenMP and MPI parallel environments in CMake. Through specific example code and <span>CMakeLists.txt</span> configuration files, it demonstrates how to use CMake’s modern features (especially imported targets) to simplify the use of OpenMP and MPI. Additionally, methods compatible with older versions of CMake are provided to ensure portability across different CMake versions. This article aims to help understand the principles of configuring OpenMP and MPI in CMake and provide best practice guidelines to improve the development efficiency of parallel programs.

2. Configuring OpenMP Parallel Environment

In today’s computing environments, multi-core processors have become standard configurations. To fully utilize these hardware resources and enhance program execution efficiency, especially for performance-sensitive applications, adopting parallel programming models is essential. OpenMP (Open Multi-Processing) is a widely used application programming interface (API) that supports shared memory multi-processor programming and is one of the standards for achieving parallel computing.

One significant advantage of OpenMP is that it allows programmers to parallelize existing code by adding preprocessor directives (also known as compilation directives or pragma) without making extensive modifications or completely rewriting the program structure. Once performance bottlenecks in the code are identified using analysis tools, programmers can insert OpenMP directives in these areas to instruct the compiler to generate parallel executable code, thus gaining performance benefits on multi-core processors.

This section will detail how to detect and enable OpenMP support in the CMake build system to compile and link programs containing OpenMP directives. A simple C++ program will be used as an example, which utilizes OpenMP for parallel computation. For relatively recent versions of CMake (especially 3.9 or higher), CMake provides very good and simplified support for OpenMP.

Note: Not all compilers support OpenMP by default. For example, on some Linux distributions, the default version of the Clang compiler may not include OpenMP support. To use OpenMP on macOS, it may be necessary to install a non-Apple version of Clang (for example, Clang installed via Homebrew or Conda) or use the GNU Compiler (GCC). Additionally, if using Clang, it may also be necessary to install the <span>libomp</span> library separately (for example, by using <span>brew install libomp</span> or obtaining it from resources like https://iscinumpy.gitlab.io/post/omp-on-high-sierra/).

2.1. Preparation

To enable OpenMP functionality in C or C++ programs, include the <span>omp.h</span> header file and enable OpenMP compilation options during compilation, linking to the corresponding OpenMP runtime library. The compiler will recognize OpenMP compilation directives in the code and generate parallel code accordingly.

In this example, the following C++ source code (<span>example.cpp</span>) will be built. This code calculates the sum of integers from 1 to N, where N is passed as a command-line argument. It utilizes OpenMP’s <span>parallel for</span> directive to parallelize the summation loop and uses the <span>reduction</span> clause to safely accumulate the result.

// example.cpp
#include <iostream>
#include <string>
#include <omp.h>    // Include OpenMP API header file

int main(int argc, char *argv[])
{
    // Check the number of command-line arguments
    if (argc < 2) {
        std::cerr << "Usage: " << argv[0] << " <number_N>\n";
        return 1;
    }

    // Print the number of available processor cores
    std::cout << "Number of available processors: " << omp_get_num_procs()
                << std::endl;
    // Print the maximum number of threads the current program can use (usually controlled by OMP_NUM_THREADS environment variable, otherwise defaults to system default)
    std::cout << "Number of threads: " << omp_get_max_threads() << std::endl;

    // Convert command-line argument to long long N
    long long n = 0;
    try {
        n = std::stoll(argv[1]); // Use std::stoll to ensure support for large numbers
    } catch (const std::out_of_range & oor) {
        std::cerr << "Error: Number N is too large or too small.\n";
        return 1;
    } catch (const std::invalid_argument & ia) {
        std::cerr << "Error: Invalid number N provided.\n";
        return 1;
    }

    std::cout << "We will form sum of numbers from 1 to " << n << std::endl;

    // Start timer, get current wall clock time
    auto t0 = omp_get_wtime();

    // Initialize summation variable
    long long s = 0LL; // Use long long to avoid overflow of large sums

    // OpenMP parallel region: parallelize the for loop
    // #pragma omp parallel for: instructs the compiler to execute the following for loop in parallel
    // reduction(+:s): specifies a reduction operation on variable s. Each thread computes its partial sum, and finally all threads' partial sums are accumulated into s.
    #pragma omp parallel for reduction(+ : s)
    // Loop variable also uses long long
    for (long long i = 1; i <= n; ++i) {
        s += i;
    }

    // Stop timer
    auto t1 = omp_get_wtime();

    // Print result and elapsed time
    std::cout << "Sum: " << s << std::endl;
    std::cout << "Elapsed wall clock time: " << t1 - t0 << " seconds" << std::endl;

    return 0;
}

The core of this code is the <span>omp.h</span> header file and the <span>#pragma omp parallel for reduction(+ : s)</span> directive.

2.2. Specific Steps

Steps to configure OpenMP support in the <span>CMakeLists.txt</span> file:

(1) First, specify the minimum required CMake version for the project. For modern support of OpenMP (via imported targets), it is recommended to use CMake 3.9 or higher. Then declare the project name and the programming language used (C++).

# Set the minimum required CMake version for the project. It is recommended to use 3.9 or higher for modern support of OpenMP.
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)

# Define the project name, specifying the language as C++
project(OpenMP_Example LANGUAGES CXX)

(2) To ensure code portability and use modern C++ features, it is recommended to explicitly set the C++ standard.

# Enforce the use of C++11 standard. The example code uses C++11 features (such as auto, std::stoll).
set(CMAKE_CXX_STANDARD 11)
# Disable compiler extensions to ensure compliance with the standard
set(CMAKE_CXX_EXTENSIONS OFF)
# Fail configuration if the standard requirements are not met
set(CMAKE_CXX_STANDARD_REQUIRED ON)

(3) CMake provides a built-in module <span>FindOpenMP.cmake</span> to detect OpenMP support on the system. By calling <span>find_package(OpenMP REQUIRED)</span>, CMake will attempt to find OpenMP compiler support and runtime libraries.

# Find OpenMP support. The REQUIRED keyword indicates that OpenMP is mandatory; if not found, CMake configuration fails.
find_package(OpenMP REQUIRED)

# Print found OpenMP information for debugging
if(OpenMP_FOUND)
    message(STATUS "Found OpenMP: ${OpenMP_CXX_FLAGS}")
    # OpenMP_CXX_FLAGS contains the flags required for the compiler to enable OpenMP (e.g., -fopenmp, /openmp)
else()
    message(FATAL_ERROR "OpenMP support not found. Please ensure your compiler supports OpenMP and it's properly configured.")
endif()

(4) Define the executable target and add the source code file (<span>example.cpp</span>). The critical step is to link to the OpenMP library. In CMake 3.9 and higher, the <span>FindOpenMP</span> module provides an imported target named <span>OpenMP::OpenMP_CXX</span> that encapsulates all the flags, include directories, and library paths required for compiling and linking OpenMP.

# Add an executable target using our C++ source file
add_executable(example example.cpp)

# Link the executable target to the OpenMP library.
# The PUBLIC keyword indicates that this linking information is not only used for the current target but will also be passed to other targets that depend on this target.
# OpenMP::OpenMP_CXX is the imported target provided by the FindOpenMP module, which contains all necessary compilation and linking information.
target_link_libraries(example
  PUBLIC
      OpenMP::OpenMP_CXX
)

The complete <span>CMakeLists.txt</span> file:

# Set the minimum required CMake version for the project. It is recommended to use 3.9 or higher for modern support of OpenMP.
cmake_minimum_required(VERSION 3.10 FATAL_ERROR)

# Define the project name, specifying the language as C++
project(OpenMP_Example LANGUAGES CXX)

# Enforce the use of C++11 standard and disable compiler extensions to ensure compliance with the standard
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Find OpenMP support. The REQUIRED keyword indicates that OpenMP is mandatory.
find_package(OpenMP REQUIRED)

# Print found OpenMP information for debugging
if(OpenMP_FOUND)
    message(STATUS "Found OpenMP: ${OpenMP_CXX_FLAGS}")
else()
    message(FATAL_ERROR "OpenMP support not found. Please ensure your compiler supports OpenMP and it's properly configured.")
endif()

# Add an executable target using our C++ source file
add_executable(example example.cpp)

# Link the executable target to the OpenMP library.
# OpenMP::OpenMP_CXX is the imported target provided by the FindOpenMP module, which contains all necessary compilation and linking information.
target_link_libraries(example
  PUBLIC
      OpenMP::OpenMP_CXX
)

Configure, build, and run this CMake project:

(1) Create a build directory and enter it, then run CMake for configuration:

mkdir build
cd build
cmake ..

If CMake successfully finds OpenMP support, you will see output similar to the following:

-- 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 OpenMP_CXX: -fopenmp (found version "4.5") 
-- Found OpenMP: TRUE (found version "4.5")  
-- Found OpenMP: -fopenmp
-- Configuring done
-- Generating done

(2) Execute the build and run the executable for parallel testing:

cmake --build .
./example 1000000000

You will see output similar to the following (specific numbers depend on your CPU core count and performance):

Number of available threads: 16
Number of threads: 16
We will calculate the sum of 1 to 1000000000
Sum = 500000000500000000
Elapsed wall clock time: 0.159563 seconds

For comparison, you can limit the number of threads used by OpenMP by setting the <span>OMP_NUM_THREADS</span> environment variable, for example, setting it to 1 to simulate serial execution:

env OMP_NUM_THREADS=1 ./example 1000000000

The output will show <span>Number of threads: 1</span>, and the <span>Elapsed wall clock time</span> will significantly increase, for example:

Number of available threads: 16
Number of threads: 1
We will calculate the sum of 1 to 1000000000
Sum = 500000000500000000
Elapsed wall clock time: 0.94243 seconds

By comparing the elapsed time of the two runs, you can visually see the performance improvement brought by OpenMP parallelization.

2.3. In-Depth Analysis of How It Works

  1. <span>find_package(OpenMP REQUIRED)</span>: This command calls CMake’s built-in <span>FindOpenMP.cmake</span> module. This module is responsible for detecting OpenMP support in C and C++ compilers on the system. It checks whether the compiler supports OpenMP flags (e.g., <span>-fopenmp</span> for GCC/Clang, <span>/openmp</span> for MSVC) and attempts to compile and link a small test program to verify whether OpenMP functionality is available. If found successfully, it sets <span>OpenMP_FOUND</span> to true and sets some variables, such as <span>OpenMP_CXX_FLAGS</span>, which contains the compilation flags required for the C++ compiler to enable OpenMP.

  2. Imported Target (<span>OpenMP::OpenMP_CXX</span>): This is the modern and recommended way to handle external library dependencies in CMake 3.9 and higher. When <span>find_package(OpenMP)</span> is successful, it creates one or more imported targets. For C++, this imported target is <span>OpenMP::OpenMP_CXX</span>. An imported target is a “pseudo-target” that does not represent source code in your project but represents a library that already exists or will be available at build time. This imported target encapsulates all the interface properties related to OpenMP, such as:

  • <span>INTERFACE_COMPILE_OPTIONS</span><span>: Contains the compiler flags required to enable OpenMP (e.g., </span><code><span>-fopenmp</span><span>).</span>
  • <span>INTERFACE_INCLUDE_DIRECTORIES</span><span>: If OpenMP requires specific header file paths, they will be included here.</span>
  • <span>INTERFACE_LINK_LIBRARIES</span><span>: Contains the linking information for the OpenMP runtime library. By using </span><code><span>target_link_libraries(example PUBLIC OpenMP::OpenMP_CXX)</span><span>, we tell CMake:</span>
  • <span>example</span> target needs to link to the OpenMP library represented by <span>OpenMP::OpenMP_CXX</span>.
  • <span>PUBLIC</span> keyword indicates that all interface properties (compilation options, include directories, link libraries) of <span>OpenMP::OpenMP_CXX</span> will not only apply to the compilation and linking of the <span>example</span> target itself but will also be passed to any other targets that directly or indirectly depend on the <span>example</span> target. This greatly simplifies dependency management in large projects.
  • This approach greatly simplifies the <span>CMakeLists.txt</span>. There is no longer a need to manually obtain <span>OpenMP_CXX_FLAGS</span> and pass them to <span>target_compile_options</span> and <span>target_link_libraries</span>. All complexity is encapsulated in the <span>OpenMP::OpenMP_CXX</span> imported target, and CMake automatically handles the correct compilation flags and link libraries. This makes build scripts cleaner, more robust, and reduces errors caused by manually managing compiler-specific flags.

  • If you want to see what properties the imported target <span>OpenMP::OpenMP_CXX</span> contains, you can use the <span>CMakePrintHelpers</span> module’s <span>cmake_print_properties</span> command (for debugging purposes only):

  • # Include CMakePrintHelpers module
    include(CMakePrintHelpers)
    
    # Print the interface properties of the OpenMP::OpenMP_CXX imported target
    cmake_print_properties(
        TARGETS
            OpenMP::OpenMP_CXX
        PROPERTIES
            INTERFACE_COMPILE_OPTIONS
            INTERFACE_INCLUDE_DIRECTORIES
            INTERFACE_LINK_LIBRARIES
            # Other properties can also be printed, such as LOCATION
        )
    

    After running CMake, you will see the values of these properties in the configuration output, for example, <span>INTERFACE_COMPILE_OPTIONS</span><span> may show </span><code><span>-fopenmp</span><span>.</span>

    2.4. Compatibility with Older Versions of CMake

    For versions of CMake below 3.9, the <span>FindOpenMP</span> module may not provide imported targets. In this case, you need to manually apply the variables set by the <span>FindOpenMP</span> module to the target. This involves using the <span>OpenMP_CXX_FLAGS</span> variable:

    # Old method only applicable for CMake < 3.9
    # Find OpenMP (imported targets may not be provided in older versions)
    find_package(OpenMP REQUIRED)
    
    add_executable(example example.cpp)
    
    # Manually add compiler options to enable OpenMP
    target_compile_options(example
      PUBLIC
          ${OpenMP_CXX_FLAGS}
      )
    
    # In some older versions of CMake or specific compilers, you may also need to manually set link flags
    # set_target_properties(example
    #   PROPERTIES
    #       LINK_FLAGS ${OpenMP_CXX_FLAGS}
    #   )
    
    # Link OpenMP library (older versions usually provide via OpenMP_CXX_LIBRARIES)
    target_link_libraries(example
      PUBLIC
          ${OpenMP_CXX_LIBRARIES} # or directly use ${OpenMP_LIBRARIES}
      )
    

    It is strongly recommended to upgrade to CMake 3.9 or higher and adopt the modern imported target approach, as it is more concise, robust, and better handles cross-compiler and platform differences.

    By correctly configuring CMake to detect and link OpenMP, you can easily enable parallel computing in C/C++ projects, fully utilizing the performance potential of multi-core processors.

    3. Configuring MPI Parallel Environment

    In the field of high-performance computing (HPC), parallel programming is key to leveraging the powerful computing capabilities of multi-core processors and distributed systems. The Message Passing Interface (MPI) is the practical standard for distributed memory parallel programming, allowing programs to communicate and cooperate through explicit message passing between multiple independent computing nodes (or multiple processes on the same node).

    MPI can be seen as a powerful complement to OpenMP (shared memory parallelism). While the latest MPI implementations may also support shared memory optimizations, its core advantage lies in handling distributed memory systems, where each processor has its own independent memory space. In high-performance computing, a common hybrid programming model is to combine MPI and OpenMP: MPI is used for communication between different computing nodes, while OpenMP is used for parallelization on shared memory processors within each node.

    A typical MPI implementation usually includes the following core components:

    1. Runtime Library: Provides the actual implementation of MPI functions for applications to call.
    2. Header Files: Contains declarations of MPI functions and definitions of constants for use by C/C++ and Fortran compilers.
    3. Compiler Wrappers: This is an important feature of the MPI environment. They are special scripts or programs that encapsulate the underlying C/C++ or Fortran compilers. For example, <span>mpicxx</span><span> (or </span><code><span>mpiCC</span><span>/</span><code><span>mpic++</span><span>) for C++, </span><code><span>mpicc</span><span> for C, </span><code><span>mpifort</span><span> for Fortran. These wrappers automatically add the additional compiler flags, header file paths, and library paths required to compile and link MPI programs, greatly simplifying the compilation process of MPI programs.</span>
    4. Launchers: Used to start the parallel execution of MPI programs. These tools (such as <span>mpirun</span><span>, </span><code><span>mpiexec</span><span>, or </span><code><span>orterun</span><span>) are responsible for starting multiple instances of the program on specified numbers of processes and nodes and establishing the communication infrastructure between them.</span>

    This section will detail how to find the appropriate MPI implementation in the CMake build system and compile a simple “Hello, World” MPI program.

    3.1. Preparation

    Using the classic “Hello, World” MPI example program (<span>helloMPI.cpp</span>). This program initializes the MPI environment, retrieves the rank of the current process in the communication group (rank) and the total size of the communication group (size), as well as the name of the processor on which the current process is running, and then prints a message.

    // helloMPI.cpp
    #include <iostream>
    #include <mpi.h>    // Include MPI library header file
    
    int main(int argc, char **argv)
    {
    // 1. Initialize MPI environment
    // MPI_Init must be called first before any MPI calls.
    // The argc and argv parameters are usually not directly used by MPI implementations but are retained for compatibility.
      MPI_Init(&amp;argc, &amp;argv); // Recommended to pass argc and argv
    
    // 2. Get the total number of processes in the communication group (usually MPI_COMM_WORLD)
    int world_size;
    // MPI_Comm_size function is used to get the number of processes in a given communicator.
    // MPI_COMM_WORLD is a predefined communicator that includes all started MPI processes.
      MPI_Comm_size(MPI_COMM_WORLD, &amp;world_size);
    
    // 3. Get the rank of the current process in the communication group
    int world_rank;
    // MPI_Comm_rank function is used to get the unique rank of the current process in a given communicator.
    // Ranks range from 0 to world_size - 1.
      MPI_Comm_rank(MPI_COMM_WORLD, &amp;world_rank);
    
    // 4. Get the name of the processor on which the current process is running
    char processor_name[MPI_MAX_PROCESSOR_NAME]; // Define a sufficiently large character array to store the processor name
    int name_len;                               // Store the actual length of the processor name
    // MPI_Get_processor_name function is used to get the name of the physical processor on which the current process is running.
      MPI_Get_processor_name(processor_name, &amp;name_len);
    
    // 5. Print "Hello World" message
    // Each MPI process will execute this code and print its own information.
    std::cout << "Hello world from processor " << processor_name
                << ", rank " << world_rank
                << " out of " << world_size << " processors" << std::endl;
    
    // 6. Finalize MPI environment
    // MPI_Finalize must be called last after any MPI calls.
    // After this call, no further MPI calls can be made.
      MPI_Finalize();
    
    return 0;
    }
    

    This program demonstrates the basic flow of MPI programming: initialization, obtaining process information, executing parallel tasks (in this case, printing), and final cleanup.

    3.2. Specific Steps

    In CMake, finding and configuring the MPI environment will use the standard module <span>FindMPI.cmake</span>.

    (1) First, define the minimum required CMake version for the project. For modern support of MPI (via imported targets), it is recommended to use CMake 3.9 or higher. Then declare the project name and the programming language used (C++).

    # Set the minimum required CMake version for the project. It is recommended to use 3.9 or higher for modern support of MPI.
    cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
    
    # Define the project name, specifying the language as C++
    project(MPI_Example LANGUAGES CXX)
    
    # Enforce the use of C++11 standard. The example code uses C++11 features.
    set(CMAKE_CXX_STANDARD 11)
    # Disable compiler extensions to ensure compliance with the standard
    set(CMAKE_CXX_EXTENSIONS OFF)
    # Fail configuration if the standard requirements are not met
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    

    (2) CMake provides the <span>FindMPI.cmake</span> module to detect MPI implementations on the system. Calling <span>find_package(MPI REQUIRED)</span> will attempt to find the libraries, header files, and compiler wrappers for MPI.

    # Find MPI support. The REQUIRED keyword indicates that MPI is mandatory; if not found, CMake configuration fails.
    # The FindMPI module will attempt to find MPI implementations for C, C++, and Fortran.
    find_package(MPI REQUIRED)
    
    # Print found MPI information for debugging
    if(MPI_FOUND)
        message(STATUS "Successfully found MPI:")
        message(STATUS "  MPI Version: ${MPI_VERSION}")
        if(MPI_CXX_FOUND)
            message(STATUS "  C++ MPI Compiler: ${MPI_CXX_COMPILER}")
            message(STATUS "  C++ MPI Includes: ${MPI_CXX_INCLUDE_PATH}")
            message(STATUS "  C++ MPI Libraries: ${MPI_CXX_LIBRARIES}")
        else()
            message(WARNING "C++ MPI support not found, but general MPI was found. This might indicate an issue if you plan to use C++.")
        endif()
        # You can also print the MPI launcher, such as MPI_EXECUTABLE
        if(MPI_EXECUTABLE)
            message(STATUS "  MPI Launcher: ${MPI_EXECUTABLE}")
        endif()
    else()
        message(FATAL_ERROR "MPI support not found. Please ensure an MPI implementation is installed and configured.")
    endif()
    

    (3) Define the executable target and add the source code file (<span>helloMPI.cpp</span>). Similar to OpenMP, the <span>FindMPI</span> module in CMake 3.9 and higher provides an imported target named <span>MPI::MPI_CXX</span> that encapsulates all the flags, include directories, and library paths required for compiling and linking MPI C++ programs.

    # Add an executable target using our C++ source file
    add_executable(helloMPI helloMPI.cpp)
    
    # Link the executable target to the MPI library.
    # The PUBLIC keyword indicates that this linking information is not only used for the current target but will also be passed to other targets that depend on this target.
    # MPI::MPI_CXX is the imported target provided by the FindMPI module, which contains all necessary compilation and linking information.
    target_link_libraries(helloMPI
      PUBLIC
          MPI::MPI_CXX
    )
    

    The complete <span>CMakeLists.txt</span> file is as follows:

    # Set the minimum required CMake version for the project. It is recommended to use 3.9 or higher for modern support of MPI.
    cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
    
    # Define the project name, specifying the language as C++
    project(MPI_Example LANGUAGES CXX)
    
    # Enforce the use of C++11 standard and disable compiler extensions to ensure compliance with the standard
    set(CMAKE_CXX_STANDARD 11)
    set(CMAKE_CXX_EXTENSIONS OFF)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
    # Find MPI support. The REQUIRED keyword indicates that MPI is mandatory.
    find_package(MPI REQUIRED)
    
    # Print found MPI information for debugging
    if(MPI_FOUND)
        message(STATUS "Successfully found MPI:")
        message(STATUS "  MPI Version: ${MPI_VERSION}")
        if(MPI_CXX_FOUND)
            message(STATUS "  C++ MPI Compiler: ${MPI_CXX_COMPILER}")
            message(STATUS "  C++ MPI Includes: ${MPI_CXX_INCLUDE_PATH}")
            message(STATUS "  C++ MPI Libraries: ${MPI_CXX_LIBRARIES}")
        else()
            message(WARNING "C++ MPI support not found, but general MPI was found. This might indicate an issue if you plan to use C++.")
        endif()
        if(MPI_EXECUTABLE)
            message(STATUS "  MPI Launcher: ${MPI_EXECUTABLE}")
        endif()
    else()
        message(FATAL_ERROR "MPI support not found. Please ensure an MPI implementation is installed and configured.")
    endif()
    
    # Add an executable target using our C++ source file
    add_executable(helloMPI helloMPI.cpp)
    
    # Link the executable target to the MPI library.
    # MPI::MPI_CXX is the imported target provided by the FindMPI module, which contains all necessary compilation and linking information.
    target_link_libraries(helloMPI
      PUBLIC
          MPI::MPI_CXX
    )
    

    Configure, build, and run this CMake project:

    mkdir build
    cd build
    cmake ..
    

    Important Note: Although the <span>FindMPI</span> module usually correctly detects the MPI compiler wrappers, in some cases, if the system’s default C++ compiler is not the MPI wrapper (for example, if the default is <span>g++</span><span> instead of </span><code><span>mpicxx</span><span>), you need to explicitly specify the compiler during CMake configuration:</span>

    cmake .. -D CMAKE_CXX_COMPILER=$(which mpicxx)
    # Or directly specify the path, for example:
    # cmake .. -D CMAKE_CXX_COMPILER=/usr/bin/mpicxx
    

    This ensures that CMake uses the MPI-provided wrapper for compilation, correctly handling MPI-specific header files and libraries.

    If CMake successfully finds MPI support, you will see output similar to the following:

    -- The CXX compiler identification is GNU 11.3.0 
    -- Check for working CXX compiler: /usr/bin/c++ 
    -- Check for working CXX compiler: /usr/bin/c++ -- works
    -- Detecting CXX compiler ABI info
    -- Detecting CXX compiler ABI info - done
    -- Detecting CXX compile features
    -- Detecting CXX compile features - done
    -- Successfully found MPI:
    --   MPI Version: 3.1
    --   C++ MPI Compiler: /usr/bin/mpicxx 
    --   C++ MPI Includes: /usr/lib/x86_64-linux-gnu/openmpi/include 
    --   C++ MPI Libraries: /usr/lib/x86_64-linux-gnu/openmpi/lib/libmpi_cxx.so;/usr/lib/x86_64-linux-gnu/openmpi/lib/libmpi.so 
    --   MPI Launcher: /usr/bin/mpirun 
    -- Configuring done
    -- Generating done
    

    Execute the build:

    cmake --build .
    

    To execute this program in parallel, use the MPI launcher (such as <span>mpirun</span><span> or </span><code><span>mpiexec</span><span>). The </span><code><span>-np</span> parameter specifies the number of processes to start.

    mpirun -np 2 ./helloMPI
    

    You will see output similar to the following (the order of process printing may vary):

    Hello world from processor larry, rank 1 out of 2 processors
    Hello world from processor larry, rank 0 out of 2 processors
    

    This indicates that two independent MPI processes have been successfully started and executed the program at different ranks.

    3.3. In-Depth Analysis of How It Works

    (1) MPI Compiler Wrappers (<span>mpicxx</span><span>, </span><code><span>mpicc</span><span>, </span><code><span>mpifort</span><span>):</span> MPI compiler wrappers are a core abstraction of the MPI environment. They are not actual compilers but encapsulations of the underlying system compilers (such as GCC, Clang, Intel C++ Compiler). When using <span>mpicxx</span><span> to compile MPI C++ programs, it actually calls the underlying C++ compiler and automatically adds all necessary compilation flags (e.g., paths for finding MPI header files) and linking flags (e.g., paths and options for linking MPI libraries).</span>

    This mechanism greatly simplifies the compilation process of MPI programs, as there is no need to manually manage complex compiler and linker options. You can view the compilation and linking flags they add behind the scenes using the <span>mpicxx --showme:compile</span><span> and </span><code><span>mpicxx --showme:link</span><span> commands.</span>

    • <span>mpicxx --showme:compile</span> will show the flags that need to be added during the compilation phase, such as thread support and the include paths for MPI header files.
    • <span>mpicxx --showme:link</span> will show the flags that need to be added during the linking phase, such as thread support, runtime library search paths (<span>-rpath</span><span>), library search paths (</span><code><span>-L</span><span>), and the MPI libraries to link (</span><code><span>-lmpi_cxx</span><span>, </span><code><span>-lmpi</span><span>).</span>

    (2) <span>find_package(MPI REQUIRED)</span> command calls CMake’s built-in <span>FindMPI.cmake</span> module. This module attempts to locate the installation path of MPI on the system and identify its components (compiler wrappers, libraries, header files). It performs a series of tests (for example, attempting to compile a simple MPI program) to verify whether the MPI environment is available. If found successfully, it sets <span>MPI_FOUND</span> to true and fills a series of MPI-related variables, such as <span>MPI_VERSION</span>, <span>MPI_CXX_COMPILER</span>, <span>MPI_CXX_INCLUDE_PATH</span>, <span>MPI_CXX_LIBRARIES</span>, etc.

    (3) Imported Target (<span>MPI::MPI_CXX</span>): This is the modern and recommended way to handle external library dependencies in CMake 3.9 and higher. When <span>find_package(MPI)</span> is successful, it creates one or more imported targets. For C++, this imported target is usually <span>MPI::MPI_CXX</span>.

    An imported target is a “pseudo-target” that does not represent source code in your project but represents a library that already exists or will be available at build time. This imported target encapsulates all the interface properties related to MPI, such as:

    • <span>INTERFACE_COMPILE_OPTIONS</span><span>: Contains the compiler flags required to enable MPI.</span>
    • <span>INTERFACE_INCLUDE_DIRECTORIES</span><span>: Contains the MPI header file paths.</span>
    • <span>INTERFACE_LINK_LIBRARIES</span><span>: Contains the linking information for the MPI runtime library.</span>

    By using <span>target_link_libraries(helloMPI PUBLIC MPI::MPI_CXX)</span><span>, we tell CMake:</span>

    • <span>helloMPI</span> target needs to link to the MPI library represented by <span>MPI::MPI_CXX</span>.
    • <span>PUBLIC</span> keyword indicates that all interface properties (compilation options, include directories, link libraries) of <span>MPI::MPI_CXX</span> will not only apply to the <span>helloMPI</span> target itself but will also be passed to any other targets that directly or indirectly depend on the <span>helloMPI</span> target. This makes dependency management more concise and automated.

    (4) This approach greatly simplifies the <span>CMakeLists.txt</span>. There is no longer a need to manually obtain <span>MPI_CXX_COMPILE_FLAGS</span>, <span>MPI_CXX_INCLUDE_PATH</span>, and <span>MPI_CXX_LIBRARIES</span> variables and pass them to <span>target_compile_options</span>, <span>target_include_directories</span>, and <span>target_link_libraries</span>. All complexity is encapsulated in the <span>MPI::MPI_CXX</span> imported target, and CMake automatically handles the correct compilation flags, include directories, and link libraries. This makes build scripts cleaner, more robust, and reduces errors caused by manually managing compiler-specific flags.

    3.4. Compatibility with Older Versions

    For versions of CMake below 3.9, the <span>FindMPI</span> module does not provide imported targets. In this case, you need to manually apply the variables set by the <span>FindMPI</span> module to the target. This involves using the <span>MPI_CXX_COMPILE_FLAGS</span>, <span>MPI_CXX_INCLUDE_PATH</span>, and <span>MPI_CXX_LIBRARIES</span> variables:

    # Old method only applicable for CMake < 3.9
    # Find MPI
    find_package(MPI REQUIRED)
    
    add_executable(helloMPI helloMPI.cpp)
    
    # Manually add compiler options to enable MPI
    target_compile_options(helloMPI
      PUBLIC
          ${MPI_CXX_COMPILE_FLAGS}
      )
    
    # Manually add MPI header file directories
    target_include_directories(helloMPI
      PUBLIC
          ${MPI_CXX_INCLUDE_PATH}
      )
    
    # Manually link MPI libraries
    target_link_libraries(helloMPI
      PUBLIC
          ${MPI_CXX_LIBRARIES}
      )
    

    It is strongly recommended to upgrade to CMake 3.9 or higher and adopt the modern imported target approach, as it is more concise, robust, and better handles cross-compiler and platform differences.

    By correctly configuring CMake to detect and link MPI, you can easily build distributed parallel applications in C/C++ projects, fully utilizing the powerful computing capabilities of clusters and multi-node systems.

    4. Conclusion

    This article has covered how to effectively detect, configure, and use OpenMP and MPI, two key parallel programming models, in the CMake build system. By using CMake’s modern features, especially imported targets (<span>OpenMP::OpenMP_CXX</span> and <span>MPI::MPI_CXX</span>), the compilation and linking processes of OpenMP and MPI programs can be greatly simplified, improving the maintainability and portability of build scripts.

    For OpenMP, use the <span>find_package(OpenMP)</span> command to find OpenMP support and integrate OpenMP functionality into C++ projects by linking to the <span>OpenMP::OpenMP_CXX</span> imported target. A simple parallel summation example demonstrates how to use OpenMP and the performance improvements brought by parallelization.

    For MPI, use the <span>find_package(MPI)</span> command to find the MPI implementation and integrate MPI functionality into C++ projects by linking to the <span>MPI::MPI_CXX</span> imported target. A classic “Hello, World” MPI example program illustrates the basic usage of MPI and how to run parallel programs using MPI launchers.

    Additionally, the workings of MPI compiler wrappers and how the <span>FindMPI</span> module detects the MPI environment and sets related CMake variables have been explored. Methods for manually configuring OpenMP and MPI for compatibility with older versions of CMake have also been provided.

    Previous tutorials

    Customized learning plans + code reviews! The C++ training camp helps you grow quickly!

    Selected

    C++20 Ranges, CMake tips, essence of programming languages

    STL

    Search algorithms: One line of code replaces 10 lines of loops, try it and see!

    Build

    CMake builds seamlessly link C/C++ with Python projects

    Struggling with Parallel Programming? CMake Provides a One-Click Solution!Lion RyanWelcome to follow my public account for learning technology or submissions

    Leave a Comment