跳到主要内容

丁致宇第二周学习报告:编译工具

​ 作者:丁致宇 时间:2024.1.27

目录

[TOC]

正文

CMake学习

CMake简介

  1. 什么是CMake? CMake(英文 Cross platform Make 的缩写)它不是构建系统,而是构建系统生成器,属于一个跨平台构建工具,在 Linux 平台生成构建系统 make 的 Makefile 文件,在 Windows 平台生成 Visual Studio 或 MSVC 的工程等。所以具体的构建工作还是需要交给例如 Make,Ninja,MSVC 等这些构建系统去执行。

  2. 什么是CMakeLists.txt? CMakeLists.txt 是 CMake 用来描述项目构建规则和过程的重要文件。它就像是一份构建指令清单,告诉 CMake 如何编译和链接代码。在 CMakeLists.txt 中,我们可以设置项目名称、指定需要编译的源文件、添加库文件依赖等等。CMake 会读取这个文件并生成对应的编译文件,指导整个构建过程。编写 CMakeLists.txt 可以让跨平台编译变得更加简单方便。

CMake基础语法

cmake_minimum_required 设置cmake的最低版本要求 示例:

cmake_minimum_required(VERSION 3.22)

project 可以用来指定工程的名字和支持的语言,默认支持所有语言 PROJECT(HELLO)指定了工程的名字,并且支持所有语言(大力推荐) PROJECT(HELLO CXX)指定了工程的名字,并且支持的语言是c++ PROJECT(HELLO C CXX)指定了工程的名字,并且支持的语言是c和c++ 示例:

project(Test)

set关键字 用于设置变量的值。这个变量可以为普通变量,缓冲变量或者是环境变量。

SET(<variable> <value>... [PARENT_SCOPE]) #设置普通变量
SET(<variable> <value>... CACHE <type> <docstring> [FORCE]) #设置缓存变量
SET(ENV{<variable>} [<value>]) #设置环境变量

add_executable 生成一个可执行文件(executable,可执行的)

ADD_EXECUTABLE(hello ${SRC_LIST})

在这行代码中,hello是生成可执行文件的文件名,文件的内容取的变量SRC_LIST的内容

MESSAGE关键字

向终端输出用户自定义的信息(个人感觉类似于c语言中printf())

主要包含三种信息:

SEND_ERROR,产生错误,生成过程被跳过。

STATUS,输出前缀为-的信息。

FATAL_ERROR,立即终止所有cmake过程。

举例: Message( STATUS "module path = ${CMAKE_MODULE_PATH}" ) 这段代码在CMake中用于输出一条状态信息到控制台。具体来说:

  • Message 是CMake的一个命令,它允许在构建过程中显示消息给用户。
  • STATUS 参数指定了消息的类型,这里是“状态”信息,通常这类信息是告知性的,不会中断构建过程,但会在标准输出(stdout)或CMake GUI界面中显示。
  • "module path = ${CMAKE_MODULE_PATH}" 是要显示的消息内容,其中 ${CMAKE_MODULE_PATH} 是一个预定义的CMake变量,它包含了CMake查找模块文件时搜索路径的列表。

CMake实践

用cmake实现OpenMP、MPI、CUDA程序的编译

cmake_minimum_required(VERSION 3.8 FATAL_ERROR)
project(HelloPrograms LANGUAGES CXX CUDA)

# 寻找OpenMP
find_package(OpenMP REQUIRED)

# CUDA程序
add_executable(hello_cuda hello_cuda.cu)

# MPI程序
find_package(MPI REQUIRED)
include_directories(SYSTEM ${MPI_INCLUDE_PATH})
add_executable(hello_mpi hello_mpi.cpp)
target_link_libraries(hello_mpi MPI::MPI_CXX)

# OpenMP程序
add_executable(hello_omp hello_omp.cpp)
target_link_libraries(hello_omp OpenMP::OpenMP_CXX)

makefile学习

makefile简介

Makefile是Linux和类Unix系统中用于自动化构建的工具make所使用的配置文件。它定义了如何编译和链接程序的目标文件(.o)、依赖关系以及命令规则。通过解析Makefile,make能够自动检测哪些源文件发生了变化,并执行相应的编译操作,避免了手动输入复杂的编译命令,极大地提高了开发效率。

makefile基础语法

  1. 目标(Targets)
  • 目标通常对应于一个可执行文件或中间文件,如:target: dependencies。例如 hello: hello.o 表示“hello”这个目标依赖于“hello.o”。
  1. 依赖(Dependencies)
  • 依赖文件是一系列在生成目标之前需要先存在的文件。当依赖文件比目标文件新或者目标不存在时,make会执行相关规则进行更新。
  1. 命令规则(Commands)
  • 命令行写在依赖关系之后,每行以制表符(tab键)开始。例如:
    target: dependencies
    command1
    command2
    这里,command1command2是在依赖文件发生改变后用来重建目标的命令。
  1. 变量(Variables)
  • 变量用于存储重复使用的值,例如编译器、编译选项等。
    CC = gcc
    CFLAGS = -Wall -g
    target: dependencies
    $(CC) $(CFLAGS) -c source.c -o target.o
  1. 隐式规则与模式规则(Implicit Rules and Pattern Rules)
  • Make自带了一些预定义的隐式规则,如.c.o:用于编译C源码文件到目标文件。
  • 模式规则允许你用一种简练的方式来描述一组具有相同编译模式的文件。
  1. 清理目标(Clean Target)
  • 通常定义一个clean目标来清除所有生成的中间文件和最终目标文件,如:
    clean:
    rm -f *.o target

makefile更多使用方法、技巧

关于$^

在 Makefile 中,$^ 是一个自动变量,它代表了当前规则中所有的依赖项(prerequisites),也就是冒号 : 右边列出的所有项。这些依赖项被空格分隔,且不包括重复的依赖项。

例如,如果你有一个 Makefile 规则如下:

myprogram: main.o utils.o
gcc -o myprogram $^

这里的 $^ 将会被展开成 main.o utils.o。所以,上面的规则相当于:

myprogram: main.o utils.o
gcc -o myprogram main.o utils.o

使用 $^ 的好处是你不必手动重复所有的依赖项,这使得 Makefile 更加简洁和易于维护。如果依赖项列表发生变化,你只需要更新依赖项列表,而不是每个地方都要修改。


常见的伪目标:

需要在这些伪目标前添加 .PHONY 声明,以告诉 make 这些目标不是文件名:

.PHONY: all clean install uninstall distclean test help
  1. all: 通常是默认目标,用来编译所有的程序。如果你只输入 make 命令,通常 all 目标会被执行。

    all: prog1 prog2
  2. clean: 如之前所述,用来删除所有由 make 创建的文件。

    clean:
    rm -f *.o prog1 prog2
  3. checktest: 用来运行程序的测试套件。

    test: all
    ./test_script.sh

注意: 这里面除了all这个伪目标外都需要使用make 目标名来触发使用


在项目中存在多级文件夹结构时可以用以下的方法处理

  1. 源文件路径

    • 如果源文件位于不同子目录下,可以在依赖关系中明确写出完整路径。例如,如果src/subdir1目录下有source1.c,则可以这样编写规则:
      target: src/subdir1/source1.o
      $(CC) $(CFLAGS) -o target $^
  2. 相对路径和绝对路径

    • 在命令行中引用文件时,可以使用相对路径(相对于Makefile的位置)或绝对路径。如果目标文件和依赖文件不在同一目录下,可能需要调整输出目录或者编译命令中的 -o 参数来指定输出位置。

makefile实践

用makefile实现OpenMP、MPI、CUDA程序的编译

# 变量定义
CC=gcc
CXX=g++
NVCC=nvcc
MPICC=mpicxx
CFLAGS=-fopenmp

# 默认目标
all: hello_cuda hello_mpi hello_omp

# CUDA程序
hello_cuda: hello_cuda.cu
$(NVCC) -o hello_cuda hello_cuda.cu

# MPI程序
hello_mpi: hello_mpi.cpp
$(MPICC) -o hello_mpi hello_mpi.cpp

# OpenMP程序
hello_omp: hello_omp.cpp
$(CC) $(CFLAGS) -o hello_omp hello_omp.cpp

# 清理目标
.PHONY: clean
clean:
rm -f hello_cuda hello_mpi hello_omp

# 测试生成的可执行文件
.PHONY: test
test:
./hello_omp
./hello_mpi
./hello_cuda

makefile实践简化实践

自动变量简化基本命令块

简化前

myprogram: main.o utils.o
gcc -o myprogram main.o utils.o

简化后

myprogram: main.o utils.o
gcc -o $@ $^

自动变量$@表示目标文件(在这里是myprogram),$^表示所有的依赖文件(在这里是main.o utils.o

每一个.c文件都编译为可执行文件

如果您需要将文件夹中的每个.c文件单独编译成一个可执行文件,您可以在Makefile中使用模式规则来完成这个任务。以下是一个简化的Makefile示例,它会找到当前目录下所有的.c文件,并为每个文件编译生成一个同名的可执行文件(不包含.c扩展名):

# 定义编译器
CC := gcc

# 定义编译选项
CFLAGS := -Wall

# 查找当前目录下所有的.c文件
SOURCES := $(wildcard *.c)

# 将.c文件列表转换成可执行文件列表
EXECUTABLES := $(SOURCES:.c=)

# 默认目标
all: $(EXECUTABLES)

# 模式规则:如何从.c文件生成可执行文件
%: %.c
$(CC) $(CFLAGS) -o $@ $<

# 伪目标,比如清理
.PHONY: clean

clean:
rm -f $(EXECUTABLES)

解释一下这个Makefile:

  1. CC变量定义了使用的编译器,这里是gcc
  2. CFLAGS变量定义了编译时使用的选项,这里使用-Wall打开所有警告。
  3. SOURCES变量使用wildcard函数获取当前目录下所有的.c文件。
  4. EXECUTABLES变量将SOURCES变量中的文件名后缀从.c改为无后缀,即可执行文件名。
  5. all是默认目标,它依赖于所有的可执行文件。
  6. %: %.c是一个模式规则,它告诉make如何从每个.c文件生成一个可执行文件。$@代表目标文件名,$<代表第一个依赖项,即对应的.c文件。
  7. .PHONY定义了一个伪目标clean,以确保即使存在名为clean的文件,执行make clean也会执行清理命令,这里它会删除所有的可执行文件。

使用这个Makefile,只需在命令行中运行make,它会自动为每个.c文件编译生成对应的可执行文件。运行make clean将会删除这些可执行文件。

Makefile真实实践

详细解读一下下面这个makefile

# We will benchmark you against Intel MKL implementation, the default processor vendor-tuned implementation.
# This makefile is intended for the Intel C compiler.
# Your code must compile (with icc) with the given CFLAGS. You may experiment with the OPT variable to invoke additional compiler options.

CC = gcc
# OPT = -no-multibyte-chars
FLAGS = -O3
# -fopt-info
LDLIBS = -lpthread -lm -llapack -lopenblas -lrt -lcblas -I/usr/include/openblas -lgptl -lpapi -O
targets = benchmark-test benchmark-naive benchmark-blocked benchmark-blas
objects = benchmark-test.o benchmark.o gemm-naive.o gemm-blocked.o gemm-blas.o


.PHONY : default
default : all

.PHONY : all
all : clean $(targets)

benchmark-test : benchmark-test.o gemm-blocked.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)

benchmark-naive : benchmark.o gemm-naive.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)
benchmark-blocked : benchmark.o gemm-blocked.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)
benchmark-blas : benchmark.o gemm-blas.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)
%.o : %.c
$(CC) -c $(CFLAGS) $(FLAGS) $<

.PHONY : clean
clean:
rm -f $(targets) $(objects)

分析

这个Makefile是用来编译和链接一个基于矩阵乘法的基准测试程序的,可能有几个不同版本的矩阵乘法实现(例如朴素的、分块的和使用BLAS库的)。下面是对Makefile中各个部分的详细解读:

首先,Makefile中的注释说明了这个Makefile是为Intel C编译器设计的,但实际上在Makefile中指定的编译器是gcc。它还提到了可以通过修改OPT变量来尝试不同的编译器选项。

CC = gcc 
# OPT = -no-multibyte-chars
FLAGS = -O3
# -fopt-info
LDLIBS = -lpthread -lm -llapack -lopenblas -lrt -lcblas -I/usr/include/openblas -lgptl -lpapi -O
  • CC:指定编译器为gcc。
  • OPT:这是一个注释掉的变量,可以用来添加额外的编译器选项,但在当前的Makefile中没有使用。
  • FLAGS:指定编译器优化级别为-O3,这是编译器的一个常用优化选项,用于启用高级优化。
  • LDLIBS:指定链接器选项,包括需要链接的库。这里包括了多线程库pthread、数学库m、LAPACK库、OpenBLAS库、实时库rt、CBLAS库、GPTL性能分析库和PAPI性能API库。-I/usr/include/openblas是一个编译器选项,用于指定OpenBLAS库的头文件位置。注意这里的-O可能是一个错误,因为它通常是一个编译器优化标志,而不是链接器标志。

接下来,Makefile定义了目标和对象文件:

targets = benchmark-test benchmark-naive benchmark-blocked benchmark-blas
objects = benchmark-test.o benchmark.o gemm-naive.o gemm-blocked.o gemm-blas.o
  • targets:这些是Makefile要构建的最终可执行文件。
  • objects:这些是编译源文件后生成的中间对象文件。

Makefile中还有一些伪目标,这些目标不对应文件名,而是执行特定的命令:

.PHONY : default
default : all

.PHONY : all
all : clean $(targets)
  • .PHONY:这个声明告诉make这些目标不是文件名。
  • default:这是默认目标,当你只输入make命令时执行的目标。这里它依赖于all目标。
  • all:这个目标首先执行clean命令来删除所有的目标和对象文件,然后构建所有的targets

下面是具体的规则来构建每个目标:

benchmark-test : benchmark-test.o gemm-blocked.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)

benchmark-naive : benchmark.o gemm-naive.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)
benchmark-blocked : benchmark.o gemm-blocked.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)
benchmark-blas : benchmark.o gemm-blas.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)

这里每个目标都有自己的依赖,然后使用$(CC)(gcc)来链接对象文件和库,生成最终的可执行文件。

对于每个.c文件的编译规则:

%.o : %.c
$(CC) -c $(CFLAGS) $(FLAGS) $<
  • %是一个通配符,这条规则告诉make如何从每个.c文件生成.o文件。这里$<代表依赖列表中的第一个项,即对应的.c文件。

最后,clean目标用于清理构建过程中生成的所有文件:

.PHONY : clean
clean:
rm -f $(targets) $(objects)

这个规则删除了所有的目标和对象文件,确保下一次构建是从干净的状态开始。

总结:这个Makefile定义了一系列规则来编译和链接基于不同矩阵乘法实现的基准测试程序。它使用了伪目标来组织构建过程,并提供了清理构建产物的功能。

疑问

在Makefile中,规则的顺序并不总是决定它们被执行的顺序。Makefile使用依赖关系来确定哪些规则需要首先执行。当你尝试构建一个目标时(比如一个可执行文件),make会查看这个目标的依赖(通常是一些对象文件),然后寻找如何构建这些依赖的规则。这意味着,即使编译生成.o文件的规则在Makefile的后面,只要有目标需要这些对象文件,make就会先执行相关的规则来生成它们。

在你提供的Makefile中,当我们尝试构建benchmark-testbenchmark-naivebenchmark-blockedbenchmark-blas这些目标时,make会寻找它们依赖的.o文件。例如:

benchmark-test : benchmark-test.o gemm-blocked.o
$(CC) -o $@ $^ $(FLAGS) $(LDLIBS)

在尝试构建benchmark-test之前,make需要benchmark-test.ogemm-blocked.o这两个对象文件。如果这些文件不存在或源文件比现有的对象文件更新,make会寻找如何构建这些对象文件的规则。在这个Makefile中,这个规则是:

%.o : %.c
$(CC) -c $(CFLAGS) $(FLAGS) $<

这条规则告诉make如何从.c文件生成.o文件。%是一个模式匹配符,它会匹配任何目标和依赖之间共有的部分。这里,$<表示所有依赖中的第一个,即对应的.c文件。

因此,即使这个规则在Makefile的最后,它仍然会在构建可执行文件之前先被执行,因为可执行文件的构建依赖于对象文件。这是make的一个基本特性:它会根据依赖关系和文件的时间戳来确定哪些规则需要被执行,而不是仅仅基于它们在Makefile中出现的顺序。

使用sh脚本进行编译

普通的sh脚本编译

#!/bin/bash

# 指定源文件名
SOURCE="my_program.cpp"

# 指定输出可执行文件名
EXECUTABLE="my_program"

# 使用mpicxx编译器(通常为OpenMPI或MPICH提供的MPI版本的C++编译器)
# 同时添加OpenMP支持(-fopenmp),链接MPI库(-lmpi)
mpicxx -o $EXECUTABLE $SOURCE -fopenmp -lmpi -std=c++11

# 检查编译是否成功
if [ $? -eq 0 ]; then
echo "Compilation successful."
else
echo "Compilation failed."
fi

# 如果编译成功,可以运行程序(假设是并行执行)
# mpirun -np <num_procs> ./my_program
# 其中<num_procs>是你想要使用的处理器核心数

更高级的sh脚本编译

#!/bin/bash

# Set the paths for MPI, OpenMP, and CUDA compilers
# Please adjust these variables according to your environment
MPI_COMPILER=mpicc
OPENMP_COMPILER=gcc
CUDA_COMPILER=nvcc

# Set the source file names
MPI_SOURCE="your_mpi_program.c"
OPENMP_SOURCE="your_openmp_program.c"
CUDA_SOURCE="your_cuda_program.cu"

# Set the output names
MPI_OUTPUT="mpi_program"
OPENMP_OUTPUT="openmp_program"
CUDA_OUTPUT="cuda_program"

# Compile MPI program
echo "Compiling MPI program..."
$MPI_COMPILER $MPI_SOURCE -o $MPI_OUTPUT
if [ $? -eq 0 ]; then
echo "MPI program compiled successfully."
else
echo "Error compiling MPI program."
exit 1
fi

# Compile OpenMP program
echo "Compiling OpenMP program..."
$OPENMP_COMPILER -fopenmp $OPENMP_SOURCE -o $OPENMP_OUTPUT
if [ $? -eq 0 ]; then
echo "OpenMP program compiled successfully."
else
echo "Error compiling OpenMP program."
exit 1
fi

# Compile CUDA program
echo "Compiling CUDA program..."
$CUDA_COMPILER $CUDA_SOURCE -o $CUDA_OUTPUT
if [ $? -eq 0 ]; then
echo "CUDA program compiled successfully."
else
echo "Error compiling CUDA program."
exit 1
fi

echo "All programs compiled successfully."

保存这个脚本为compile.sh,然后在终端中运行chmod +x compile.sh来给予执行权限。运行脚本前,请确保你已经正确设置了脚本中的编译器路径和源文件名。

这个shell脚本中,$?变量包含了上一条命令的退出状态。如果命令成功执行,它会返回0,否则返回非0值。脚本中的if语句会检查每个编译命令是否成功,如果有任何编译失败,脚本将打印错误信息并退出。

这个脚本假设源代码文件不需要任何额外的库或者特定的编译器选项。如果程序依赖于特定的库或者需要额外的编译选项,需要相应地修改编译命令。例如,如果CUDA程序需要链接到某些库,可能需要添加-l选项到nvcc命令中。

类似makefile的编译脚本

#!/bin/bash

# 变量定义
CC=gcc
CXX=g++
NVCC=nvcc
MPICC=mpicxx
CFLAGS=-fopenmp

# 默认目标
function build_all {
build_hello_cuda
build_hello_mpi
build_hello_omp
}

# CUDA程序
function build_hello_cuda {
$NVCC -o hello_cuda hello_cuda.cu
}

# MPI程序
function build_hello_mpi {
$MPICC -o hello_mpi hello_mpi.cpp
}

# OpenMP程序
function build_hello_omp {
$CC $CFLAGS -o hello_omp hello_omp.cpp
}

# 清理目标
function clean {
rm -f hello_cuda hello_mpi hello_omp
}

# 测试生成的可执行文件
function test {
./hello_omp
./hello_mpi
./hello_cuda
}

# 解析命令行参数
function main {
case "$1" in
clean)
clean
;;
test)
test
;;
*)
build_all
;;
esac
}

# 调用主函数
main $1

给它执行权限:

chmod +x build.sh

通过以下命令运行:

./build.sh         # 编译所有程序
./build.sh clean # 清理编译产生的文件
./build.sh test # 测试生成的可执行文件

这个脚本提供了与Makefile相同的功能,包括编译CUDA、MPI和OpenMP程序,清理编译产生的文件,以及测试编译出的程序。