目录
Makefile实战
前言
学习完了杜老师推荐的Makefile教程视频,链接。来个实战先。
针对https://github.com/shouxieai/infer仓库中的Makefile进行分析和修改,完成该项目在Jetson nano上的编译和运行。
1.介绍
infer框架是杜老师最近推出的全新tensorrt封装,轻易继承各类任务,其优点有
- 轻易实现各类任务的生产者消费者模型,并进行高性能推理
- 没有复杂的封装,彻底解开耦合!
除去多余的文件,完整的目录结构如下
- src
- 存放源文件,包括6个文件,分别为
cpm.hpp
、infer.hpp
以及yolo.hpp
三个头文件;infer.cu
和yolo.cu
两个CUDA文件;main.cpp
一个源文件
- 存放源文件,包括6个文件,分别为
- workspace
- inference
- 存放待推理的图片
- build.sh,脚本文件,利用
trtexec
工具将ONNX模型编译生成engine - v8trans.py,python文件,用于在yolov8的onnx模型中添加transpose节点
- yolov8n.transd.onnx和yolov8n-seg.b1.transd.onnx为yolov8导出的检测和分割模型
- inference
- Makefile
说明
-
我们尽量只关注Makefile文件中的内容,先分析原Makefile内容然后修改满足我们的需求
-
文件虽然少但是包含的内容多,具体包括C++和CUDA的混合编程,以及tensorRT和OpenCV等库文件的链接,还是非常值得学习的。
-
本次使用的设备是Jetson nano嵌入式
-
对于该项目感兴趣的可以查看如何高效使用TensorRT视频
-
对于该项目应用在Jetson nano嵌入式感兴趣的可以查看Jetson nano部署YOLOv8
2.Makefile文件分析
首先来看下该仓库的Makefile文件具体内容:
cc := g++
nvcc = /root/.kiwi/lib/cuda-11.8/bin/nvcc
include_paths := src \
/root/.kiwi/lib/opencv4.2/include \
/root/.kiwi/lib/TensorRT-8.5.3.1-cuda11x/include \
/root/.kiwi/lib/cuda-11.8/include \
/root/.kiwi/lib/cudnn8.6.0.163-cuda11/lib
library_paths := /root/.kiwi/lib/opencv4.2/lib \
/root/.kiwi/lib/TensorRT-8.5.3.1-cuda11x/lib \
/root/.kiwi/lib/cuda-11.8/lib64 \
/root/.kiwi/lib/cudnn8.6.0.163-cuda11/lib
link_librarys := opencv_core opencv_imgproc opencv_videoio opencv_imgcodecs \
nvinfer nvinfer_plugin nvonnxparser \
cuda cublas cudart cudnn \
stdc++ dl
cppstrict := -Wall -Werror -Wextra -Wno-deprecated-declarations -Wno-unused-parameter
custrict := -Werror=all-warnings
cpp_compile_flags := -std=c++11 -fPIC -g -fopenmp $(cppstrict) -O0
cu_compile_flags := -std=c++11 $(custrict) -O0 -Xcompiler "$(cpp_compile_flags)"
link_flags := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'
include /root/.kiwi/lib/cumk/inc
这个Makefile文件的内容并不非常完整,只定义了一些变量和搜索路径相关,后续修改还需要添加编译和链接的内容。以下是对每个部分的详细分析
1.定义编译器和nvcc路径
cc := g++
nvcc = /root/.kiwi/lib/cuda-11.8/bin/nvcc
其中:
cc
是用于编译C++代码的GNU C++编译器;nvcc
是用于编译CUDA代码的NVIDIA CUDA编译器
2.定义include路径、库路径和链接的库名
include_paths := src \
/root/.kiwi/lib/opencv4.2/include \
/root/.kiwi/lib/TensorRT-8.5.3.1-cuda11x/include \
/root/.kiwi/lib/cuda-11.8/include \
/root/.kiwi/lib/cudnn8.6.0.163-cuda11/lib
library_paths := /root/.kiwi/lib/opencv4.2/lib \
/root/.kiwi/lib/TensorRT-8.5.3.1-cuda11x/lib \
/root/.kiwi/lib/cuda-11.8/lib64 \
/root/.kiwi/lib/cudnn8.6.0.163-cuda11/lib
link_librarys := opencv_core opencv_imgproc opencv_videoio opencv_imgcodecs \
nvinfer nvinfer_plugin nvonnxparser \
cuda cublas cudart cudnn \
stdc++ dl
其中:
include_paths
定义了需要编译的代码的头文件所在路径;library_paths
定义了库文件的路径;link_librarys
定义了需要链接的库名。
3.定义编译选项
cppstrict := -Wall -Werror -Wextra -Wno-deprecated-declarations -Wno-unused-parameter
custrict := -Werror=all-warnings
cpp_compile_flags := -std=c++11 -fPIC -g -fopenmp $(cppstrict) -O0
cu_compile_flags := -std=c++11 $(custrict) -O0 -Xcompiler "$(cpp_compile_flags)"
link_flags := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'
其中,
cppstrict
是用于编译C++代码的编译警告选项;custrict
是用于编译CUDA代码的编译警告选项;cpp_compile_flags
是用于编译C++代码的编译选项,包括C++11标准、位置独立代码、调试信息、OpenMP多线程编程等;cu_compile_flags
是用于编译CUDA代码的编译选项,包括C++11标准、位置独立代码、关闭所有警告、调试信息等;link_flags
是用于链接库的选项,包括多线程编程和动态链接库路径
4.包含一个第三方库
include /root/.kiwi/lib/cumk/inc
3.Makefile文件修改
修改的地方有点多,下面具体来分析
3.1 定义编译器
cc := g++
nvcc := /usr/local/cuda-10.2/bin/nvcc
其中,
cc := g++
定义了C++编译器变量cc
nvcc := /usr/local/cuda-10.2/bin/nvcc
定义了CUDA编译器变量nvcc
3.2 定义源文件和目标文件
cpp_srcs := $(shell find src -name "*.cpp")
cpp_objs := $(cpp_srcs:.cpp=.cpp.o)
cpp_objs := $(cpp_objs:src/%=objs/%)
cpp_mk := $(cpp_objs:.cpp.o=.cpp.mk)
cu_srcs := $(shell find src -name "*.cu")
cu_objs := $(cu_srcs:.cu=.cu.o)
cu_objs := $(cu_objs:src/%=objs/%)
cu_mk := $(cu_objs:.cu.o=.cu.mk)
其中,
cpp_srcs := $(shell find src -name "*.cpp")
使用shell命令查找源代码src目录下的所有.cpp文件,并将结果保存到cpp_srcs
变量中cpp_objs := $(cpp_srcs:.cpp=.cpp.o)
将cpp_srcs
中的每个.cpp文件中的.cpp替换为.cpp.o,为了将所有的.cpp文件编译成.o文件cpp_objs := $(cpp_objs:src/%=objs/%)
将cpp_objs
中所有的obj文件的路径前缀从src/替换为objs/,即将所有的.o文件放在objs目录下cpp_mk := $(cpp_objs:.cpp.o=.cpp.mk)
将所有的.cpp.o文件替换为.cpp.mk,即为每个.cpp源文件生成一个依赖文件cu_srcs
、cu_objs
、cu_mk
同理,只是操作的是src下面的所有.cu文件
说明
-
$(cpp_srcs:.cpp=.cpp.o)
这是make中一种字符串语法,语法结构如下$(VAR:a=b)
其中,
VAR
为变量名,a
和b
均为字符串,表示将VAR
中所有以a
结尾的字符串替换为b
结尾的字符串。相比于patsubst
函数,这种语法更加简洁,适用于只需要进行简单字符串替换的情况。 -
$(cpp_objs:.cpp.o=.cpp.mk)
其中的.mk
文件用来记录源文件和头文件的依赖关系。- 在C/C++项目中,一个.cpp文件往往包含了多个.h头文件,这些头文件的修改也会影响到.cpp文件的编译,因此正常情况下需要在头文件被修改时重新编译.cpp文件。
.mk
文件记录了每个源文件所依赖的头文件,以及头文件的修改时间,从而实现自动化依赖关系的生成。因此在修改头文件重新编译时,只需要重新编译与头文件相关的源文件即可。这可以大大减少编译时间,尤其在大型项目中尤为明显,如果没有.mk
文件,每次重新编译时都需要重新编译整个项目,这将非常耗费时间和资源- 在Makefile中使用
-include
命令可以将.mk
文件包含进来,实现了Makefile的自动化构建过程
3.3 定义头文件路径、库文件路径和库名称
include_paths := src \
/usr/include/opencv4 \
/usr/include/aarch64-linux-gnu \
/usr/local/cuda-10.2/include
library_paths := /usr/lib/aarch64-linux-gnu \
/usr/local/cuda-10.2/lib64
link_librarys := opencv_core opencv_highgui opencv_imgproc opencv_videoio opencv_imgcodecs \
nvinfer nvinfer_plugin nvonnxparser \
cuda cublas cudart cudnn \
stdc++ dl
其中,
-
include_paths
定义了所有头文件所在的路径,主要包含以下几方面内容- src文件下的.hpp文件
- opencv头文件
- tensorRT、cudnn头文件
- cuda头文件
-
library_paths
定义了所有库文件所在的路径,主要包含以下几方面的内容- opencv、tensorRT、cudnn库文件
- cuda库文件
-
link_librarys
定义了需要链接的所有库文件,主要包含以下几方面的内容- opencv库文件:opencv_core、opencv_highgui、opencv_imgproc、opencv_videoio、opencv_imgcodecs
- tensorRT库文件:nvinfer、nvinfer_plugin、nvonnxparser
- cuda库文件:cuda、cublas、cudart
- cudnn库文件:cudnn
- GNU C++标准库:stdc++;动态链接库:dl
3.4 定义导出和运行时路径
empty :=
export_path := $(subst $(empty) $(empty),:,$(library_paths))
run_paths := $(foreach item,$(library_paths),-Wl,-rpath=$(item))
其中,
export_path := $(subst $(empty) $(empty),:,$(library_paths))
将library_paths
中的所有路径用冒号连接起来,用于在链接时指定动态库搜索路径run_paths := $(foreach item,$(library_paths),-Wl,-rpath=$(item))
将library_paths
中的每个路径添加上链接选项-Wl,-rpath
,用于在链接时指定动态库搜索路径
说明
-
以下是
subst
函数的语法结构,针对$(subst $(empty) $(empty),:,$(library_paths))
这个语句来说<from>代表的是$(empty) $(empty)
这一整体内容,$(empty)
代表空字符串,不是空格,也没有其它任何字符。$(empty) $(empty)
是为了产生一个空格的效果。$(subst <from>,<to>,<text>)
-
$(subst $(empty) $(empty),:,$(library_paths))
的整体含义就是将其中的空格替换成冒号,以便链接器可以正确地查找动态库。例如,如果library_paths
地值为:/usr/lib/aarch64-linux-gnu /usr/local/cuda-10.2/lib64
那么以下几种情况对应的输出分别为:
export_path := $(subst $(empty) $(empty),:,$(library_paths)) /usr/lib/aarch64-linux-gnu:/usr/local/cuda-10.2/lib64 export_path1 := $(subst $(empty),:,$(library_paths)) /usr/lib/aarch64-linux-gnu /usr/local/cuda-10.2/lib64: export_path2 := $(subst ,:,$(library_paths)) /usr/lib/aarch64-linux-gnu /usr/local/cuda-10.2/lib64:
3.5 定义编译选项
include_paths := $(foreach item,$(include_paths),-I$(item))
library_paths := $(foreach item,$(library_paths),-L$(item))
link_librarys := $(foreach item,$(link_librarys),-l$(item))
cpp_compile_flags := -std=c++11 -fPIC -w -g -pthread -fopenmp -O0
cu_compile_flags := -std=c++11 -g -w -O0 -Xcompiler "$(cpp_compile_flags)"
link_flags := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'
cpp_compile_flags += $(include_paths)
cu_compile_flags += $(include_paths)
link_flags += $(library_paths) $(link_librarys) $(run_paths)
其中,
include_paths
是一个路径列表,通过foreach
函数将其转换为-I
开头的参数列表。这些参数用于指定头文件所在的路径。library_paths
是一个库路径列表,通过foreach
函数将其转换为-L
开头的参数列表。这些参数用于指定库文件所在的路径。link_librarys
是一个库文件列表,通过foreach
函数将其转换为-l
开头的参数列表。这些参数用于指定链接的库文件。cpp_compile_flags
是编译C++文件的编译参数列表,其中包括了``include_paths、
-fPIC(生成位置无关代码)、
-w(忽略警告)、
-g(生成调试信息)、
-pthread(链接线程库)、
-fopenmp(开启OpenMP多线程)和
-O0`(关闭优化)等选项。cu_compile_flags
是编译CUDA文件的编译参数列表,同上,-Xcompile
(指定传递给底层C++编译器的参数)link_flags
是链接参数列表,其中包括了library_paths
、link_librarys
和run_paths
等选项。run_paths
选项用于指定运行时的库路径,用于解决动态库加载问题。
说明
fPIC
(Position Independent Code),它的用途是生成与位置无关的代码。在编译生成动态库时,这个参数可以确保代码中的全局符号表、重定位表等数据的位置独立于内存中的位置,从而允许多个进程在内存中共享同一份库代码。简单来说,fPIC
是在编译生成动态库时必须加上的参数,以确保生成的动态库能够被不同的进程共享使用- 在CUDA和C++混合编程中,由于NVCC和g++编译器的编译选项有所不同,需要为它们分别指定不同的编译选项。NVCC编译器底层需要调用g++编译器,可以通过
-Xcompile
选项将参数传递给NVCC生成的C++编译器。这样,可以确保NVCC编译器和g++编译器都使用了相同的选项,从而确保了代码的正确性和一致性 -Wl,-rpath='$$ORIGIN'
表示将rpath设置为$ORIGIN
,在这里$ORIGIN
是一个特殊字符,表示可执行文件所在目录的路径。双重美元符号$$
是因为$ORIGIN
在Shell中有特殊含义,因此需要转义。
3.6 添加头文件依赖
ifneq ($(MAKECMDGOALS), clean)
-include $(cpp_mk) $(cu_mk)
endif
说明
-include
是Makefile中的内置函数,它的作用是将指定的文件包含到当前的Makefile中。上面的语句的作用是在执行make命令时,如果没有clean目标,就会包含(include)指定的文件即$(cpp_mk)
和$(cu_mk)
。而这两个.mk
文件包含了相应的源文件的依赖关系。这样在修改源文件时,make会自动判断哪些文件需要重新编译,以及哪些文件需要重新链接,从而提高了构建效率。- 如果不包含这个语句,可能导致make不能正确地识别源文件之间的依赖关系,或者需要程序编译所有文件,从而降低了构建效率。因此,在构建复杂项目时,最好包含这个语句确保构建的正确性和效率。
3.7 编译CUDA和C++程序
pro := workspace/pro
expath := library_path.txt
library_path.txt :
@echo LD_LIBRARY_PATH=$(export_path):"$$"LD_LIBRARY_PATH > $@
workspace/pro : $(cpp_objs) $(cu_objs)
@echo Link $@
@mkdir -p $(dir $@)
@$(cc) $^ -o $@ $(link_flags)
objs/%.cpp.o : src/%.cpp
@echo Compile CXX $<
@mkdir -p $(dir $@)
@$(cc) -c $< -o $@ $(cpp_compile_flags)
objs/%.cu.o : src/%.cu
@echo Compile CUDA $<
@mkdir -p $(dir $@)
@$(nvcc) -c $< -o $@ $(cu_compile_flags)
objs/%.cpp.mk : src/%.cpp
@echo Compile depends CXX $<
@mkdir -p $(dir $@)
@$(cc) -M $< -MF $@ -MT $(@:.cpp.mk=.cpp.o) $(cpp_compile_flags)
objs/%.cu.mk : src/%.cu
@echo Compile depends CUDA $<
@mkdir -p $(dir $@)
@$(nvcc) -M $< -MF $@ -MT $(@:.cu.mk=.cu.o) $(cu_compile_flags)
run : workspace/pro
@cd workspace && ./pro
其中,
pro
变量表示最终生成的可执行文件的路径expath
变量表示库文件路径的导出文件名library_path.txt
文件用于生成LD_LIBRARY_PATH
环境变量所需的值,其内容是LD_LIBRARY_PATH
和export_path
的值workspace/pro
规则用于编译链接生成最终的可执行文件。其中包含了所有的C++和CUDA文件objs/%.cpp.o
规则和objs/%.cu.o
规则分别用于编译C++和CUDA文件,生成目标文件objs/%.cpp.mk
规则和objs/%.cu.mk
规则分别用于生成C++和CUDA文件的依赖关系描述文件run
规则用于运行生成的可执行文件
说明
objs/%.cpp.mk : src/%.cpp
定义了一条规则,当在src目录下的.cpp发生变化时,会执行该规则生成对应的.cpp.mk文件$(cc) -M $< -MF $@ -MT $(@:.cpp.mk=.cpp.o) $(cpp_compile_flags)
该命令会将根据源文件生成对应的依赖关系文件-M
:表示只输出每个源文件的依赖关系,不包括编译指令,例如-M src/main.cpp
-MF
:表示将输出的依赖关系写入到指定的文件中,例如-MF objs/main.cpp.mk
-MF
:表示制定目标文件,即make要生成的文件,例如-MT objs/main.cpp.o
3.8 其它
debug :
@echo $(include_paths)
@echo $(library_paths)
@echo $(link_librarys)
clean :
@rm -rf library_path.txt
@rm -rf objs workspace/pro
@rm -rf workspace/Result.jpg
.PHONY : debug clean run
export LD_LIBRARY_PATH:=$(export_path):$(LD_LIBRARY_PATH)
其中,
clean
规则用于删除生成的目标文件、库文件和可执行文件export LD_LIBRARY_PATH
语句用于导出LD_LIBRARY_PATH
环境变量,使得运行时能够正确链接库文件。
4.完整Makefile
完整的Makefile示例如下:
cc := g++
nvcc := /usr/local/cuda-10.2/bin/nvcc
cpp_srcs := $(shell find src -name "*.cpp")
cpp_objs := $(cpp_srcs:.cpp=.cpp.o)
cpp_objs := $(cpp_objs:src/%=objs/%)
cpp_mk := $(cpp_objs:.cpp.o=.cpp.mk)
cu_srcs := $(shell find src -name "*.cu")
cu_objs := $(cu_srcs:.cu=.cu.o)
cu_objs := $(cu_objs:src/%=objs/%)
cu_mk := $(cu_objs:.cu.o=.cu.mk)
include_paths := src \
/usr/include/opencv4 \
/usr/include/aarch64-linux-gnu \
/usr/local/cuda-10.2/include
library_paths := /usr/lib/aarch64-linux-gnu \
/usr/local/cuda-10.2/lib64
link_librarys := opencv_core opencv_highgui opencv_imgproc opencv_videoio opencv_imgcodecs \
nvinfer nvinfer_plugin nvonnxparser \
cuda cublas cudart cudnn \
stdc++ dl
empty :=
export_path := $(subst $(empty) $(empty),:,$(library_paths))
run_paths := $(foreach item,$(library_paths),-Wl,-rpath=$(item))
include_paths := $(foreach item,$(include_paths),-I$(item))
library_paths := $(foreach item,$(library_paths),-L$(item))
link_librarys := $(foreach item,$(link_librarys),-l$(item))
cpp_compile_flags := -std=c++11 -fPIC -w -g -pthread -fopenmp -O0
cu_compile_flags := -std=c++11 -g -w -O0 -Xcompiler "$(cpp_compile_flags)"
link_flags := -pthread -fopenmp -Wl,-rpath='$$ORIGIN'
cpp_compile_flags += $(include_paths)
cu_compile_flags += $(include_paths)
link_flags += $(library_paths) $(link_librarys) $(run_paths)
ifneq ($(MAKECMDGOALS), clean)
-include $(cpp_mk) $(cu_mk)
endif
pro := workspace/pro
expath := library_path.txt
library_path.txt :
@echo LD_LIBRARY_PATH=$(export_path):"$$"LD_LIBRARY_PATH > $@
workspace/pro : $(cpp_objs) $(cu_objs)
@echo Link $@
@mkdir -p $(dir $@)
@$(cc) $^ -o $@ $(link_flags)
objs/%.cpp.o : src/%.cpp
@echo Compile CXX $<
@mkdir -p $(dir $@)
@$(cc) -c $< -o $@ $(cpp_compile_flags)
objs/%.cu.o : src/%.cu
@echo Compile CUDA $<
@mkdir -p $(dir $@)
@$(nvcc) -c $< -o $@ $(cu_compile_flags)
objs/%.cpp.mk : src/%.cpp
@echo Compile depends CXX $<
@mkdir -p $(dir $@)
@$(cc) -M $< -MF $@ -MT $(@:.cpp.mk=.cpp.o) $(cpp_compile_flags)
objs/%.cu.mk : src/%.cu
@echo Compile depends CUDA $<
@mkdir -p $(dir $@)
@$(nvcc) -M $< -MF $@ -MT $(@:.cu.mk=.cu.o) $(cu_compile_flags)
run : workspace/pro
@cd workspace && ./pro
debug :
@echo $(include_paths)
@echo $(library_paths)
@echo $(link_librarys)
clean :
@rm -rf library_path.txt
@rm -rf objs workspace/pro
@rm -rf workspace/Result.jpg
.PHONY : debug clean run
export LD_LIBRARY_PATH:=$(export_path):$(LD_LIBRARY_PATH)
执行make run
运行效果如下:
总结
本次实战选取开源仓库https://github.com/shouxieai/infer中的Makefile文件做了具体的分析和修改,在今后的工作中多看多写吧😄