CUPTI SASS 源代码映射教程
GitHub 仓库和完整教程可在 https://github.com/eunomia-bpf/cupti-tutorial 获取。
简介
在优化 CUDA 内核时,理解源代码如何转换为实际的 GPU 汇编指令可以提供强大的洞察。SASS(流式汇编)是 NVIDIA GPU 执行的本机汇编语言,映射 CUDA C/C++ 代码与生成的 SASS 指令之间的关系可以揭示在源代码级别不明显的优化机会。本教程演示如何使用 CUPTI 提取内核的 SASS 代码并将其映射回原始源代码,帮助您理解高级代码与 GPU 上实际执行的指令之间的关系。
您将学到什么
- 如何提取 CUDA 内核的 SASS 汇编代码
- 将 SASS 指令映射回源代码行
- 解释 SASS 以理解指令级行为
- 通过分析 SASS 识别优化机会
- 使用 SASS 信息做出明智的优化决策
理解 CUDA 编译
当您编译 CUDA 代码时,它经过几个阶段:
- CUDA C/C++:您的高级源代码
- PTX:中间表示(并行线程执行)
- SASS:GPU 执行的最终机器代码
每个阶段代表不同的抽象级别,理解最终的 SASS 代码可以提供在源代码级别不可见的洞察。
代码演练
1. 加载 CUDA 模块
首先,我们需要加载 CUDA 模块以访问其代码:
CUmodule module;
CUfunction function;
// 加载模块
DRIVER_API_CALL(cuModuleLoad(&module, "kernel.cubin"));
// 获取内核函数
DRIVER_API_CALL(cuModuleGetFunction(&function, module, "vectorAdd"));
这段代码: 1. 加载编译的 CUDA 二进制(cubin)文件 2. 获取该模块内特定内核函数的句柄
2. 提取 SASS 代码
接下来,我们提取内核的 SASS 代码:
// 获取函数的代码
CUdeviceptr code;
size_t codeSize;
DRIVER_API_CALL(cuFuncGetAttribute(&code, CU_FUNC_ATTRIBUTE_CODE, function));
DRIVER_API_CALL(cuFuncGetAttribute(&codeSize, CU_FUNC_ATTRIBUTE_BINARY_SIZE, function));
// 为代码分配内存
unsigned char *sassCode = (unsigned char *)malloc(codeSize);
if (!sassCode) {
fprintf(stderr, "为 SASS 代码分配内存失败\n");
return -1;
}
// 从设备内存复制代码
DRIVER_API_CALL(cuMemcpyDtoH(sassCode, code, codeSize));
这段代码: 1. 获取函数代码的设备指针及其大小 2. 分配内存来保存 SASS 代码 3. 将代码从设备内存复制到主机内存
3. 反汇编 SASS
现在我们将二进制 SASS 代码反汇编为人类可读的格式:
// 创建反汇编器
CUpti_Activity_DisassembleData disassembleData;
memset(&disassembleData, 0, sizeof(disassembleData));
disassembleData.size = sizeof(disassembleData);
disassembleData.cubin = sassCode;
disassembleData.cubinSize = codeSize;
disassembleData.function = (const char *)function;
// 反汇编代码
CUPTI_CALL(cuptiActivityDisassembleKernel(&disassembleData));
// 获取反汇编的 SASS
const char *sassText = disassembleData.sass;
这段代码: 1. 设置反汇编的结构 2. 调用 CUPTI 反汇编内核 3. 获取生成的 SASS 文本
4. 将 SASS 映射到源代码
要将 SASS 指令映射到源代码,我们使用 CUPTI 的行信息 API:
// 获取模块中的函数数量
uint32_t numFunctions = 0;
CUPTI_CALL(cuptiModuleGetNumFunctions(module, &numFunctions));
// 获取函数 ID
CUpti_ModuleResourceData *functionIds =
(CUpti_ModuleResourceData *)malloc(numFunctions * sizeof(CUpti_ModuleResourceData));
CUPTI_CALL(cuptiModuleGetFunctions(module, numFunctions, functionIds));
// 对于每个函数
for (uint32_t i = 0; i < numFunctions; i++) {
// 检查这是否是我们的目标函数
if (strcmp(functionIds[i].resourceName, "vectorAdd") == 0) {
// 获取行信息
uint32_t numLines = 0;
CUPTI_CALL(cuptiGetNumLines(functionIds[i].function, &numLines));
// 为行信息分配内存
CUpti_LineInfo *lineInfo =
(CUpti_LineInfo *)malloc(numLines * sizeof(CUpti_LineInfo));
// 获取行信息
CUPTI_CALL(cuptiGetLineInfo(functionIds[i].function, numLines, lineInfo));
// 处理行信息
for (uint32_t j = 0; j < numLines; j++) {
printf("偏移 0x%x 处的 SASS 指令映射到 %s:%d\n",
lineInfo[j].pcOffset, lineInfo[j].fileName, lineInfo[j].lineNumber);
}
free(lineInfo);
}
}
free(functionIds);
这段代码: 1. 获取模块中的函数列表 2. 找到我们的目标函数 3. 获取该函数的行信息 4. 将每个 SASS 指令偏移映射到源文件和行号
5. 创建源代码注释的 SASS 列表
现在我们将 SASS 代码与源行信息结合:
void printSourceAnnotatedSass(const char *sassText, CUpti_LineInfo *lineInfo, uint32_t numLines)
{
// 解析 SASS 文本
char *sassCopy = strdup(sassText);
char *line = strtok(sassCopy, "\n");
int currentSourceLine = -1;
const char *currentFileName = NULL;
// 处理每行 SASS
while (line != NULL) {
// 提取指令偏移
unsigned int offset;
if (sscanf(line, "/*%x*/", &offset) == 1) {
// 为此偏移找到源行
for (uint32_t i = 0; i < numLines; i++) {
if (lineInfo[i].pcOffset == offset) {
// 如果我们移动到新的源行,打印它
if (currentSourceLine != lineInfo[i].lineNumber ||
currentFileName != lineInfo[i].fileName) {
currentSourceLine = lineInfo[i].lineNumber;
currentFileName = lineInfo[i].fileName;
// 读取源文件并获取行
char sourceLine[1024];
FILE *sourceFile = fopen(currentFileName, "r");
if (sourceFile) {
for (int j = 0; j < currentSourceLine; j++) {
if (!fgets(sourceLine, sizeof(sourceLine), sourceFile)) {
break;
}
}
fclose(sourceFile);
// 移除换行符
sourceLine[strcspn(sourceLine, "\n")] = 0;
printf("\n源代码第 %d 行:%s\n", currentSourceLine, sourceLine);
}
}
break;
}
}
}
// 打印 SASS 指令
printf(" %s\n", line);
// 获取下一行
line = strtok(NULL, "\n");
}
free(sassCopy);
}
这个函数: 1. 解析 SASS 文本行 2. 提取每条指令的偏移 3. 找到对应的源行 4. 当移动到新源行时打印源代码 5. 缩进打印 SASS 指令
实际应用
性能热点分析
通过源代码映射的 SASS,您可以:
- 识别昂贵的源行:查看哪些源代码行生成了最多的指令
- 分析内存访问模式:理解内存操作如何在汇编级别实现
- 优化算法:看到编译器如何转换您的算法
示例输出
源代码第 42 行:float a = A[i];
/*0008*/ LDG.E.SYS R2, [R8];
源代码第 43 行:float b = B[i];
/*0010*/ LDG.E.SYS R4, [R10];
源代码第 44 行:float result = a + b;
/*0018*/ FADD R6, R2, R4;
源代码第 45 行:C[i] = result;
/*0020*/ STG.E.SYS [R12], R6;
这个输出显示: - 每个源行如何转换为特定的 SASS 指令 - 内存加载操作(LDG)用于读取数组元素 - 浮点加法(FADD)执行计算 - 内存存储操作(STG)写入结果
优化洞察
指令效率
通过检查 SASS,您可以识别:
- 冗余指令:编译器生成的不必要操作
- 内存合并机会:访问模式的优化潜力
- 寄存器使用:寄存器压力和溢出问题
编译器优化
// 源代码
for (int i = 0; i < N; i++) {
C[i] = A[i] + B[i];
}
// 可能生成展开的 SASS(如果编译器优化)
/*0008*/ LDG.E.64 R2, [R8]; // 加载 A[i] 和 A[i+1]
/*0010*/ LDG.E.64 R4, [R10]; // 加载 B[i] 和 B[i+1]
/*0018*/ FADD R6, R2, R4; // 添加第一对
/*0020*/ FADD R7, R3, R5; // 添加第二对
/*0028*/ STG.E.64 [R12], R6; // 存储两个结果
这显示编译器如何展开循环并使用64位加载进行向量化。
调试技术
性能回归
当性能回归时,比较 SASS 输出可以揭示:
- 指令计数变化:新版本是否生成更多指令
- 内存访问模式:访问是否变得不那么高效
- 分支行为:控制流是否变得更复杂
代码验证
void validateOptimization() {
// 比较优化前后的 SASS
char *beforeSass = extractSassForKernel("kernel_v1");
char *afterSass = extractSassForKernel("kernel_v2");
int beforeInstructions = countInstructions(beforeSass);
int afterInstructions = countInstructions(afterSass);
printf("优化前指令数:%d\n", beforeInstructions);
printf("优化后指令数:%d\n", afterInstructions);
if (afterInstructions < beforeInstructions) {
printf("✓ 优化成功减少了指令数\n");
} else {
printf("⚠ 优化增加了指令数\n");
}
}
最佳实践
设置编译标志
为了获得最佳的源映射:
# 包含调试信息
nvcc -g -lineinfo -o kernel kernel.cu
# 生成 cubin 文件
nvcc --cubin -o kernel.cubin kernel.cu
# 同时生成 PTX 以进行比较
nvcc -ptx -o kernel.ptx kernel.cu
分析工作流
- 建立基线:为当前实现创建 SASS 映射
- 实施更改:修改源代码
- 比较 SASS:查看汇编级别的差异
- 测量性能:验证 SASS 变化是否带来预期的性能提升
常见模式
// 识别内存合并问题
void checkMemoryCoalescing(const char *sass) {
if (strstr(sass, "LDG.E.32") != NULL) {
printf("发现32位标量加载 - 考虑向量化\n");
}
if (strstr(sass, "LDG.E.128") != NULL) {
printf("发现128位向量加载 - 良好的合并\n");
}
}
// 分析分支效率
void analyzeBranches(const char *sass) {
int branchCount = 0;
char *pos = (char*)sass;
while ((pos = strstr(pos, "BRA")) != NULL) {
branchCount++;
pos += 3;
}
printf("发现 %d 个分支指令\n", branchCount);
}
SASS 源代码映射为理解 CUDA 编译器行为和优化 GPU 代码提供了无价的洞察。通过桥接高级源代码和低级汇编之间的差距,您可以做出更明智的优化决策并验证您的更改产生了预期的效果。