CUDA环境配置
目前要做的项目涉及到cuda编程,于是便学习了一下cuda环境配置和cuda编程,现将这个过程总结下来,以供分享和自己查看。本文先来讲讲cuda编程的环境配置。
\qquad 首先要说明一点,由于cuda编程语言是在C++语言的基础上扩展来的,所以可以直接在.cpp文件中来编写cuda代码,对应的头文件采用.hpp,同时要记住包含头文件#include <cuda_runtime.h>,头文件中的内容其实就是cuda编程语言在C++语言的基础上扩展的部分,如果不包含这个头文件,则会缺失这一部分扩展的部分,cuda代码也就无法进行正确的编译和运行了,甚至在编写代码时就会提示报错。
\qquad 但是,随着CUDA的越来越普及,C++规定了一种新的源代码文件格式,即.cu文件,对应的头文件为.cuh文件,其两者的用法与传统的.cpp和.hpp文件的用法基本一致,唯一的区别是编译器在处理.cu和.cuh文件时会自动将编译cuda文件所需要的那一部分东西包含进来,省去了我们人为包含头文件的麻烦。当然,由于这种写法的方便性,现在基于.cu和.cuh的cuda编程依然占据主流,所以在后面的程序中我会采用.cu和.cuh的方式进行cuda编程。
\qquad 关于cuda编程的基本知识,大家可以通过其他博主的分享来学习,互联网上有很多优质的cuda编程教程。我写此文的目的在于,帮助那些已经了解过cuda编程基本概念的同学,迅速搭建起来一套可以正确运行cuda代码的环境,因为其实这件事并不简单,我自己也被折磨了很久😫。我所使用的cuda环境的搭建的硬件平台是linux服务器,cuda版本11.8,系统为ubuntu20.04 LTS,使用到一张RTX3090显卡。下面让我们直接开始。
一.单个.cu源文件的编译和运行
让我们从最简单的开始,比如如下test.cu代码:
#include <stdio.h>
__global__ void hello_from_gpu()
{
printf("Hello World from the the GPU\n");
}
int main(void)
{
hello_from_gpu<<<1, 1>>>();
cudaDeviceSynchronize();
return 0;
}
\qquad 这个文件中没有包含C++标准库以外的头文件,源文件也只有这一个,所以无需借助make和cmake等工具,可以直接编译运行。我们知道,在linux系统中,C语言是由gcc编译器编译和链接的,C++是有g++编译器进行编译和链接的。而.cu文件中的cuda代码,则是由nvidia自家开发的nvcc编译器进行编译和链接的。所以g++编译.cpp源文件时的指令基本上都可以直接迁移到用nvcc编译器来编译.cu文件上来。另外说明一点,在开始编译之前务必要确保已经正确安装cuda并且可以正确使用nvcc编译器(这里如果有问题的话可以在网上查看教程)。如果编译器已经准备就绪的话,就可以开始编译啦!
nvcc test.cu -o test
\qquad 在终端输入上述指令就可以完成编译啦,test.cu是要编译的源文件,test则是编译之后的可执行文件的名字,可以自己指定。编译好之后就会在当前目录下生成一个test可执行文件,在终端继续输入以下指令执行该文件:
./test
\qquad 可以看到打印结果为Hello World from the the GPU,大功告成。(这里提一嘴,当使用nvcc编译.cu文件时,代码中设备代码以外的标准C++部分其实还是交给g++编译的,g++编译不了的核函数和cuda语言扩展部分才会交给nvcc来编译。)
二.多文件cuda项目的编译
\qquad 这个过程就稍微有点棘手了,以我的文件结构为例,工程文件夹为test_cuda_func,其中的include文件夹用来存放项目所要用到的头文件,工程文件夹下面还有用来编译的CMakeLists.txt和cuda源代码文件test.cu,虽然文件也不是很多,但是麻雀虽小,五脏俱全,这个小项目已经具备一个多文件cuda项目的所有要素,在未来遇到更加复杂的项目的时候,只需要在此基础上做一些扩展和迁移的工作就好。
\qquad 很明显,我采用了cmake工具来编译我的cuda项目,所以要亲自去完善一个CMakeLists.txt文件。后面我们会聊到,这一块虽然不难,但是对于初学者,也是要折腾蛮久的。
test.cu文件内容如下:
#include <stdio.h>
#include <torch/extension.h>
#include <torch/torch.h>
#include "include/common.cuh"
using namespace torch;
__global__ void chunk_local_cumsum_vector_kernel(float* __restrict__ g_org, float* __restrict__ g_new, int T, int B, int H, int K, int BT, int BK, float* m_s)
{
int b = blockIdx.x; // 每个线程块处理一个 B 索引
int h = blockIdx.y; // 每个线程块处理一个 H 索引
int nt = threadIdx.x; // 当前线程在 T 维度上的索引(每个线程块处理一个 T 范围)
int nk = threadIdx.y; // 当前线程在 K 维度上的索引(每个线程块处理一个 K 范围)
// 计算每个线程应该处理的数据块的位置
int t_index = nt * BT; // 在 T 维度上的位置
int k_index = nk * BK; // 在 K 维度上的位置
for (int t = 0; t < BT; t++) {
for (int k = 0; k < BK; k++) {
// 计算全局数据索引
long int global_index = (b * H + h) * (T * K) + (t_index + t) * K + (k_index + k);
for (int m = 0; m < BT; m++) {
// 使用一维数组进行矩阵乘法,索引转换
g_new[global_index] += m_s[t * BT + m] * g_org[(b * H + h) * (T * K) + (t_index + m) * K + (k_index + k)];
}
}
}
}
int next_power_of_2(int N)
{
int R = int(std::pow(2, std::ceil(std::log2(N))));
return R;
}
torch::Tensor chunk_local_cumsum_vector(torch::Tensor g, int BT)
{
int B = int(g.size(0));
int H = int(g.size(1));
int T = int(g.size(2));
int K = int(g.size(3));
int NT = (T + BT - 1) / BT; // 计算 NT(ceiling divide)
std::cout<< torch::cuda::is_available()<<std::endl;
// 保存原始的g
torch::Tensor g_org = g.to(torch::kCUDA);
// 创建一个新的空Tensor,与g具有相同的类型和大小
torch::Tensor g_new = torch::zeros_like(g_org, torch::dtype(torch::kFloat).device(torch::kCUDA));
float* g_org_ptr = g_org.data_ptr<float>(); // 获取数据指针
float* g_new_ptr = g_new.data_ptr<float>(); // 获取数据指针
int BK = 16;
int NK = (K + BK - 1) / BK;
// triton:(NK,NT,B*H)共K*T*B*H个元素,则每个线程处理BK*BT个元素
dim3 grid(B, H); // 网格大小
dim3 block(NT, NK); // 每个块的线程数
// 创建 o_i 向量,存储从 0 到 BT-1 的值
size_t byte_num_m_s = BT * BT * sizeof(float);
int* o_i = (int *)malloc(BT * sizeof(int));
float* m_s = (float*)malloc(byte_num_m_s);
if (o_i != NULL && m_s != NULL)
{
memset(o_i, 0, BT * sizeof(int)); // 主机内存初始化为0
memset(m_s, 0, byte_num_m_s);
}
for (int i = 0; i < BT; i++) {
o_i[i] = i;
}
for (int i = 0; i < BT; ++i) {
for (int j = 0; j < BT; ++j) {
// 根据 o_i[i] >= o_i[j] 条件填充 m_s[i * BT + j]
if (o_i[i] >= o_i[j]) {
m_s[i * BT + j] = 1.0f;
} else {
m_s[i * BT + j] = 0.0f;
}
}
}
// 在设备端分配内存
float* d_m_s;
cudaMalloc((float**)&d_m_s, byte_num_m_s); // 分配 m_s 数组的设备内存
if (d_m_s != NULL)
{
cudaMemset(d_m_s, 0, byte_num_m_s); // 设备内存初始化为0
}
// 将 o_i 数组从主机复制到设备
cudaMemcpy(d_m_s, m_s, byte_num_m_s, cudaMemcpyHostToDevice);
// 调用核函数
chunk_local_cumsum_vector_kernel<<<grid, block>>>(
g_org_ptr,
g_new_ptr,
T, B, H, K, BT, BK
,d_m_s
);
cudaDeviceSynchronize(); // 确保核函数执行完成
torch::Tensor g_cumsum = g_new.to(torch::kCPU); // 使用 to(torch::kCPU) 从 GPU 拷贝到 CPU
free(o_i);
free(m_s);
cudaFree(d_m_s);
return g_cumsum;
}
int main(void)
{
torch::Tensor q = torch::randn({1, 32, 1024, 128}, torch::dtype(torch::kFloat32));
torch::Tensor k = torch::randn({1, 32, 1024, 128}, torch::dtype(torch::kFloat32));
torch::Tensor v = torch::randn({1, 32, 1024, 128}, torch::dtype(torch::kFloat32));
torch::Tensor g = torch::randn({1, 32, 1024, 128}, torch::dtype(torch::kFloat32));
torch::Tensor h0 = torch::zeros({1, 32, 128, 128}, torch::dtype(torch::kFloat32));
int BT = 64;
float scale = 0;
if (scale == 0)
{
scale = std::pow(q.size(-1), -0.5f);
}
torch::Tensor g_cumsum = chunk_local_cumsum_vector(g,BT);
return 0;
}
common.cuh文件的内容如下:
#pragma once
#include <stdlib.h>
#include <stdio.h>
#define CUDA_CHECK(call) __cudaCheck(call, __FILE__, __LINE__)
#define LAST_KERNEL_CHECK(call) __kernelCheck(__FILE__, __LINE__)
static void __cudaCheck(cudaError_t err, const char* file, const int line) {
if (err != cudaSuccess) {
printf("ERROR: %s:%d, ", file, line);
printf("CODE:%s, DETAIL:%s\n", cudaGetErrorName(err), cudaGetErrorString(err));
exit(1);
}
}
static void __kernelCheck(const char* file, const int line) {
cudaError_t err = cudaPeekAtLastError();
if (err != cudaSuccess) {
printf("ERROR: %s:%d, ", file, line);
printf("CODE:%s, DETAIL:%s\n", cudaGetErrorName(err), cudaGetErrorString(err));
exit(1);
}
}
\qquad 我们来分析一下,由于我的项目中要用到大量的张量,所以我选择pytorch的C++接口版本libtorch,来使用其中定义的torch::Tensor,所以就要包含#include <torch/torch.h>。而且,我写好的这个.cu文件,未来也会构建成共享库,来提供给另一个python程序调用,所以,就要包含#include <torch/extension.h>,这就是.cu文件中开头包含两个头文件的原因。这样一来,我的.cu源文件就包含了第三方库libtorch,所以,要想成功编译,就势必要经历手动链接第三方库对应的静态或动态链接库的过程。所以我选择使用cmake工具来编译项目,使这个过程相对简单一点。
这里不太明白的同学请容我解释一下编译一个包含了第三方库头文件的.cpp文件需要经历哪些步骤。
编译一个包含第三方库头文件的
.cpp
文件时,整个过程主要包括以下步骤:
1. 准备环境
确保以下内容就绪:
- 已安装的编译器(如
g++
、clang++
)。- 第三方库及其对应的头文件(
.h
或.hpp
)。- 如果库需要动态链接(
.so
或.dll
),需安装运行时动态库;如果静态链接,需准备静态库(.a
或.lib
)。
2. 确认第三方库的文件路径
- 头文件路径(通常包含
.h
或.hpp
文件)。- 库文件路径(通常是
.so
、.a
、.dll
或.lib
文件)。- 记下这些路径,后续编译时会用到。
例如:
头文件路径: /path/to/library/include 库文件路径: /path/to/library/lib
3. 编写代码并包含头文件
在你的代码中,使用
#include
指令包含第三方库的头文件。例如:#include <thirdparty.h> // 假设第三方库头文件名为 thirdparty.h int main() { // 使用第三方库的功能 return 0; }
确保包含路径正确,或者使用绝对路径。
4. 编译命令
编译分为以下几个步骤:
a. 指定头文件路径
使用
-I
参数告诉编译器头文件所在的路径。例如:g++ -I/path/to/library/include -c main.cpp -o main.o
b. 链接库文件
在链接阶段,指定第三方库的路径和库名:
- 使用
-L
参数:告诉编译器库文件所在路径。- 使用
-l
参数:指定需要链接的库名(去掉lib
前缀和扩展名)。例如:
g++ -L/path/to/library/lib -lthirdparty main.o -o main
若库文件为
libthirdparty.so
或libthirdparty.a
,使用-lthirdparty
即可。
5. 运行可执行文件
如果库是动态链接库(如
.so
),确保运行时能够找到它们:
将动态库所在路径添加到环境变量。例如:
复制代码 export LD_LIBRARY_PATH=/path/to/library/lib:$LD_LIBRARY_PATH
然后运行程序:
./main
6. 调试和优化
如果编译或运行失败:
- 检查路径是否正确。
- 确保库版本兼容。
- 启用调试信息(
-g
)或更详细的错误信息(-v
或--verbose
)。
总结:完整编译示例
假设:
- 第三方库头文件在
/usr/local/include
。- 第三方库文件在
/usr/local/lib
。- 库名为
libmylib.so
或libmylib.a
。完整命令:
g++ -I/usr/local/include -L/usr/local/lib -lmylib main.cpp -o main
如果使用动态库运行:
export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH ./main
\qquad 看到这里,很多小伙伴就有疑问了,那为什么我刚刚在单个.cu文件编译的时候也包含了#include <stdio.h>,但是却没有手动链接什么这库那库的,就只是一个编译命令就解决了呢,其实原因在于:
为什么不用手动链接?
标准库是默认链接的
在 C 和 C++ 中,标准库(如libc
、libm
)是编译器的默认依赖项,编译器会自动将它们加入到链接阶段,无需用户手动指定。例如,
stdlib.h
是 C 标准库的一部分,提供了基本的内存管理、程序控制等功能,它的实现通常位于libc
(标准 C 库)中。编译器知道你的程序需要这个库,因此自动链接。编译器默认配置
现代编译器(如gcc
和clang
)在构建工具链时会包含对标准库的默认搜索路径和自动链接设置。当你编译一个程序时,编译器会自动:
- 在默认路径下寻找标准库头文件。
- 在链接阶段加入必要的标准库。
标准库 ABI 的一致性
标准库的 ABI(应用二进制接口)是稳定的,编译器在运行时使用系统或工具链提供的标准库实现。这让编译器可以确保程序和标准库之间的兼容性。
\qquad 理解了上面所说的内容之后,就可以通过cmake工具来编译项目了,当然首先跑不了的就是编写CMakeLists.txt文件了。但是在此之前,我们要先把准备工作做好:安装要使用的第三方库libtorch。(同时,由于其要求编译时的C++标准为C++17及以上,所以在编译时指定的CUDA标准也得在C++17及以上并且和C++一致,这就对cmake的版本又提出了要求,所以这么一通下来,配套安装了好多东西,安装教程都可以查到)(忽略掉下面的doc,这不重要)
一切准备就绪之后,就可以来写CMakeLists.txt了,首先晒出我的:
# 最低版本要求
cmake_minimum_required(VERSION 3.10)
# 项目信息
project(test LANGUAGES CXX CUDA)
# 设置 LibTorch 的路径
set(CMAKE_PREFIX_PATH "/data/shuzhengwang/xeon_gla/env/libtorch/") # 替换为 LibTorch 的实际路径
# 查找 LibTorch
find_package(Torch REQUIRED)
# 手动指定头文件和库路径
set(TORCH_INCLUDE_DIRS "/data/shuzhengwang/xeon_gla/env/libtorch/include/")
# 添加其他必要的搜索路径
include_directories("/data/shuzhengwang/xeon_gla/env/libtorch/include/torch/csrc/api/include/")
include_directories("/data/shuzhengwang/env/anaconda3/envs/gla/include/python3.9/")
# 检查cuda
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_CUDA_STANDARD 17) # 用于指定CUDA编译器应该使用的CUDA C++标准的版本
set(CMAKE_CUDA_STANDARD_REQUIRED ON) # 表明如果找不到指定版本的CUDA编译器,将发出错误
set(CMAKE_CXX_STANDARD 17) # 用于指定 C++ 编译器应该使用的 C++ 标准版本
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 表明如果找不到指定版本的 C++ 编译器,将发出错误
set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O3 -arch=sm_86 --ptxas-options=-v")
# set(CMAKE_CUDA_FLAGS_DEBUG="-G -g -O0")
find_package(CUDA REQUIRED)
if (CUDA_FOUND)
message(STATUS "CUDA_INCLUDE_DIRS: ${CUDA_INCLUDE_DIRS}")
message(STATUS "CUDA_LIBRARIES: ${CUDA_LIBRARIES}")
message(STATUS "CUDA_LIBRARY_DIRS: ${CUDA_LIBRARY_DIRS}")
else()
message(FATAL_ERROR "Cannot find CUDA")
endif()
# 添加可执行文件
add_executable(test test.cu)
target_include_directories(test PRIVATE ${CUDA_INCLUDE_DIRS} ${TORCH_INCLUDE_DIRS})
# 动态链接库
target_link_libraries(test PRIVATE ${CUDA_LIBRARIES} ${TORCH_LIBRARIES})
下面我来为大家深度解读一下这个文件的内容,大致可以分为这几步:
-
# 最低版本要求 cmake_minimum_required(VERSION 3.10) # 项目信息 project(test LANGUAGES CXX CUDA)
指定允许使用的cmake的最低版本,防止你项目中的某些东西在别人的电脑上不能使用。
指定项目信息(项目名字,使用的编程语言)
-
# 设置 LibTorch 的路径 set(CMAKE_PREFIX_PATH "/data/shuzhengwang/xeon_gla/env/libtorch/") # 替换为 LibTorch 的实际路径 # 查找 LibTorch find_package(Torch REQUIRED) # 手动指定头文件和库路径 set(TORCH_INCLUDE_DIRS "/data/shuzhengwang/xeon_gla/env/libtorch/include/")
在安装好一个第三方库之后,无论是否已经将其路径添加到环境变量中,都最好通过set CMAKE_PREFIX_PATH来指定一下这个库的绝对路径,防止出现找不到的情况。然后使用find_package(Torch REQUIRED)确保是不是真的能找到。
set(TORCH_INCLUDE_DIRS)是在手动指定第三方库的include文件夹(头文件)所在地,其实这里一般不需要额外指定,但是为了保险,我还是指定了。
-
# 添加其他必要的搜索路径 include_directories("/data/shuzhengwang/xeon_gla/env/libtorch/include/torch/csrc/api/include/") include_directories("/data/shuzhengwang/env/anaconda3/envs/gla/include/python3.9/")
在指定好库的根路径和头文件的路径之后,还是有可能会发生某几个位置特别犄角旮旯的头文件找不到的错误,这时候,就要自己去找找这些头文件在哪里,然后用include_directories来额外指定他们的位置,这两句代码就是我根据报错找到对应的头文件然后把位置指定上去的。
-
# 检查cuda set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_CUDA_STANDARD 17) # 用于指定CUDA编译器应该使用的CUDA C++标准的版本 set(CMAKE_CUDA_STANDARD_REQUIRED ON) # 表明如果找不到指定版本的CUDA编译器,将发出错误 set(CMAKE_CXX_STANDARD 17) # 用于指定 C++ 编译器应该使用的 C++ 标准版本 set(CMAKE_CXX_STANDARD_REQUIRED ON) # 表明如果找不到指定版本的 C++ 编译器,将发出错误 set(CMAKE_CUDA_FLAGS "${CMAKE_CUDA_FLAGS} -O3 -arch=sm_86 --ptxas-options=-v") # set(CMAKE_CUDA_FLAGS_DEBUG="-G -g -O0") find_package(CUDA REQUIRED) if (CUDA_FOUND) message(STATUS "CUDA_INCLUDE_DIRS: ${CUDA_INCLUDE_DIRS}") message(STATUS "CUDA_LIBRARIES: ${CUDA_LIBRARIES}") message(STATUS "CUDA_LIBRARY_DIRS: ${CUDA_LIBRARY_DIRS}") else() message(FATAL_ERROR "Cannot find CUDA") endif()
这里就是在设置一些编译时需要用到的C++和CUDA标准,以及一些cuda编程独有的设置。例如这一句 set(CMAKE_CUDA_FLAGS “${CMAKE_CUDA_FLAGS} -O3 -arch=sm_86 --ptxas-options=-v”) 就是在指定显卡计算能力等,来确保编译成功的可执行文件可以在自己的显卡上成功运行。find_package(CUDA REQUIRED) 也是在找你安装的cuda库,由于cuda我已经在环境变量中指定的很详细了,所以在这里我试了一下,是可以做到不做额外指定直接找到cuda的。
-
# 添加可执行文件 add_executable(test test.cu include/common.cuh)
这一步很关键了,因为代码编译之后的用途不同,这里也会不同。1)如果只是想将自己的代码编译成一个可执行文件来在设备上运行,就要借助add_executable语句按照上述格式来将项目中的所有源文件一一添加进来。2)如果想要将自己的代码构建成为一个共享库,来作为第三方库供别人或者自己使用,那么这里的语句则要换成:(这里是自己踩过的雷)
set(SRC_FILES
“cuda_kernel.cu”
“include/cuda_kernel.cuh”)
add_library(cuda_kernel SHARED ${SRC_FILES})
-
target_include_directories(test PRIVATE ${CUDA_INCLUDE_DIRS} ${TORCH_INCLUDE_DIRS})
这里就是将所有要用到的来自四面八方的头文件全部整合到了一起,放在target_include_directories中,方便待会的编译。
在 CMake 中,
target_include_directories
是用来为目标指定头文件搜索路径的指令,而PRIVATE
是访问控制的修饰符,用于确定这些目录的作用范围。CMake 中还有其他两个修饰符PUBLIC
和INTERFACE
,它们的具体含义如下:
1. PRIVATE
- 作用范围:仅当前目标可见。
- 指定的包含目录只对当前目标(例如
test
)有效,不会传递给依赖它的其他目标。 - 使用场景: 头文件目录仅用于当前目标的编译,与其他目标无关。
示例:
target_include_directories(test PRIVATE ${CUDA_INCLUDE_DIRS} ${TORCH_INCLUDE_DIRS})
test
会在编译时使用${CUDA_INCLUDE_DIRS}
和${TORCH_INCLUDE_DIRS}
,但这些目录不会影响依赖于test
的目标。
2. PUBLIC
- 作用范围:当前目标和依赖当前目标的其他目标均可见。
- 指定的包含目录既适用于当前目标,也会传递给所有依赖此目标的其他目标。
- 使用场景: 当前目标的头文件依赖对其他目标也是可见且必要的。
示例:
target_include_directories(test PUBLIC ${CUDA_INCLUDE_DIRS})
test
和依赖test
的其他目标都会使用${CUDA_INCLUDE_DIRS}
。
3. INTERFACE
- 作用范围:仅依赖当前目标的其他目标可见。
- 指定的包含目录只对依赖此目标的其他目标有效,而当前目标自身并不会使用这些目录。
- 使用场景: 当前目标自身不需要这些头文件,但其他目标需要。
示例:
target_include_directories(test INTERFACE ${CUDA_INCLUDE_DIRS})
- 依赖于
test
的其他目标会使用${CUDA_INCLUDE_DIRS}
,但test
自身不会使用。
三者的区别总结
修饰符 当前目标可见 依赖目标可见 PRIVATE ✅ ❌ PUBLIC ✅ ✅ INTERFACE ❌ ✅
示例
假设有三个目标:
libA
、libB
和app
,其中:app
依赖libB
,libB
依赖libA
。libA
提供一些公共头文件,libB
也有自己的头文件。
头文件目录的定义:
target_include_directories(libA PUBLIC ${LIBA_INCLUDE_DIRS}) target_include_directories(libB PRIVATE ${LIBB_INCLUDE_DIRS}) target_include_directories(app PRIVATE ${APP_INCLUDE_DIRS})
行为解释:
libA
的头文件目录传递给了libB
和app
。libB
的头文件目录仅libB
自己可见,不会影响app
。app
的头文件目录仅app
自己可见。
为什么用
PRIVATE
?在你的例子中:
target_include_directories(test PRIVATE ${CUDA_INCLUDE_DIRS} ${TORCH_INCLUDE_DIRS})
使用
PRIVATE
的含义是:- 这些包含目录(
${CUDA_INCLUDE_DIRS}
和${TORCH_INCLUDE_DIRS}
)仅在编译test
时需要。 - 它们对其他依赖
test
的目标(如果有)是不可见的,因为这些目标不需要知道test
的实现细节(如头文件路径)。
这种封装有助于减少不必要的依赖传播,提高构建系统的效率和清晰性。
-
# 动态链接库 target_link_libraries(test PRIVATE ${CUDA_LIBRARIES} ${TORCH_LIBRARIES})
这一步就是要在链接过程中将要用到的第三方库的链接库全部都链接起来,完成这一步,就大功告成了。
第三方库的
lib
文件夹下的链接库文件和include
文件夹下的头文件是一个库的两个重要组成部分,它们的关系可以概括为:头文件提供接口声明,链接库文件提供接口实现。两者在一起才能让你完整地使用第三方库。
具体关系
-
头文件(
include
文件夹)- 包含函数、类、模板、宏等的声明或定义。
- 用于告诉编译器如何调用库的功能,例如函数原型、数据结构布局等。
- 是给开发者直接访问的接口部分(API)。
- 作用阶段: 头文件主要在编译阶段被使用,帮助编译器检查代码的语法和功能调用是否合法。
示例(假设库是数学计算库
libmath
):// math.h double add(double a, double b); // 函数声明
-
链接库文件(
lib
文件夹)- 包含头文件中声明的函数、类的实际实现。
- 链接库可能是静态库(
.a
或.lib
)或动态库(.so
或.dll
)。 - 是实际功能的二进制实现。
- 作用阶段: 链接库在链接阶段被链接到你的程序中,为程序提供实际的运行时功能。
示例:
- 静态库文件:
libmath.a
(Linux/macOS) 或math.lib
(Windows)。 - 动态库文件:
libmath.so
(Linux) 或math.dll
(Windows)。
关系和配合工作
-
头文件声明接口,链接库提供实现:
- 头文件让你知道如何使用库(例如函数原型)。
- 链接库是对头文件中声明的功能的实现支持。
- 编译器需要头文件进行语法检查,链接器需要库文件来完成二进制链接。
-
路径引用:
- 编译时通过
-I
参数指定头文件路径(include
文件夹)。 - 链接时通过
-L
参数指定库文件路径(lib
文件夹)。
编译命令示例:
g++ main.cpp -I/path/to/library/include -L/path/to/library/lib -lmylib -o main
- 编译时通过
为什么头文件和库文件要分开?
- 分离接口与实现:
- 头文件暴露了接口定义,而库文件隐藏了具体实现。
- 用户只需要头文件了解如何使用库,而不需要关心内部实现细节。
- 提高复用性和效率:
- 用户无需重新编译库文件,只需要链接预编译好的库文件,节省时间。
- 如果库的实现发生变化(例如优化代码),只需重新编译库文件,无需改变头文件(除非接口改变)。
- 封装性:
- 链接库文件通常是二进制格式(如
.so
或.dll
),用户无法直接看到源代码,从而保护了库的知识产权。
- 链接库文件通常是二进制格式(如
示例:头文件与库文件的配合
假设你使用一个数学库,目录结构如下:
mathlib/ ├── include/ │ └── math.h ├── lib/ │ ├── libmath.a │ └── libmath.so
使用流程:
-
包含头文件: 在代码中引用
math.h
,使用库提供的接口:#include <math.h> int main() { double result = add(1.0, 2.0); return 0; }
-
编译时指定头文件路径: 告诉编译器
math.h
在哪里:g++ main.cpp -I/path/to/mathlib/include
-
链接库文件: 在链接阶段将程序与
libmath.a
或libmath.so
链接:g++ main.o -L/path/to/mathlib/lib -lmath -o main
-
运行时使用动态库(如果用的是动态库): 确保运行时可以找到
libmath.so
:export LD_LIBRARY_PATH=/path/to/mathlib/lib:$LD_LIBRARY_PATH ./main
总结
include
文件夹中的头文件 提供接口定义,用于编译阶段。lib
文件夹中的库文件 提供接口实现,用于链接阶段。- 两者配合使用,共同完成程序的编译和运行。
-
至此,CMakeLists.txt就编写完毕了。下面来编译。
cmake -B build -S .
cd build
make clean(可选)
make
./test
\qquad 确保终端中当前路径为项目根目录,然后依次输入上述命令完成编译,生成可执行文件。其中make clean用于清除上一次make生成的文件,防止对该次make造成影响,建议加上。
\qquad 然后就编译完成了,尝试运行,是OK的,现在就可以去肆无忌惮的扩充代码了。