CUDA 并行编程进阶笔记 + C++示例代码(第四章重点)

      +

      一、核心笔记(整合前四章核心)

      1. SIMD 执行模型深度解析

      • 核心本质:单指令多数据,同一指令同时作用于多个数据单元,是 GPU 并行加速的核心原理。

      • 关键约束

        • 数据需连续存储:优化内存访问,充分利用 GPU 内存层级;

        • 避免分支语句:若核函数含`if-else`,线程束(warp)中部分线程会闲置(执行一个分支时,其他分支线程等待),需尽量简化分支或让分支结果一致;

        • 精度取舍优化:用`float`替代`double`、byte`替代`int,可提升数据吞吐量,间接提升性能。

      • 适用场景:无复杂分支的规整计算(如向量运算、矩阵乘法、图像像素处理),对应第一章“数据并行”核心场景。

      2. 非完全并行示例:素数测试(GPU 实现)

      核心逻辑

      • 每个 GPU 线程负责测试一个奇数(跳过偶数,减少计算量),通过全局线程 ID 映射到待测试数字;

      • 优化点:仅测试到数字的平方根(i*i <= num),避免重复检测因子对;

      • 局限性:含循环和少量分支,未充分发挥 SIMD 优势,但无需内存传输,适合入门验证核函数执行。

      关键对比(CPU vs GPU)

      测试范围 CPU 耗时(ms) GPU 耗时(ms) 加速比

      100,001-101,001

      0.1643

      0.1335

      1.23x

      100,001-110,001

      1.3839

      0.1433

      9.66x

      100,001-190,001

      15.6056

      0.2298

      67.91x

      • 结论:数据量越大,GPU 优势越明显(GPU 线程并行执行,循环开销被分摊)。

      3. GPU 内存管理核心 API(第四章重点)

      GPU 内存与 CPU 内存物理分离,需通过专用 API 管理数据传输,核心 API 如下:

      API 函数 功能 关键参数/注意事项

      cudaMalloc

      分配 GPU 设备内存(全局内存)

      第一个参数为`void**(需类型转换),第二个参数为字节数;失败返回`cudaErrorMemoryAllocation

      cudaMemcpy

      数据拷贝(CPU↔GPU)

      第四个参数指定方向:cudaMemcpyHostToDevice(CPU→GPU)、cudaMemcpyDeviceToHost(GPU→CPU);默认阻塞调用

      cudaFree

      释放 GPU 设备内存

      仅释放`cudaMalloc`分配的设备内存,避免内存泄漏

      cudaGetLastError

      获取最近一次 CUDA API 错误

      用于调试核函数启动、内存操作等错误

      内存管理最佳实践

      • 命名规范:CPU 内存前缀`h_(host),GPU 内存前缀`d_(device),避免混淆;

      • 最小化数据传输:GPU 计算耗时远小于内存传输耗时,尽量“一次传输、多次计算”;

      • 必做错误检查:内存操作易出错,需用`cudaGetLastError()`捕获错误。

      4. GPU 执行时间测量(cudaEvent)

      核函数异步执行(CPU 调用后立即返回),需用`cudaEvent`精准测量 GPU 执行时间:

      核心步骤

      1. 创建事件:cudaEventCreate(&startEvent)cudaEventCreate(&stopEvent)

      2. 记录事件:核函数执行前记录`startEvent`,执行后记录`stopEvent`;

      3. 同步事件:cudaEventSynchronize(stopEvent)(阻塞 CPU,等待 GPU 执行完成);

      4. 计算耗时:cudaEventElapsedTime(&duration, startEvent, stopEvent)(单位:毫秒);

      5. 销毁事件:cudaEventDestroy(startEvent)cudaEventDestroy(stopEvent),避免资源泄漏。

      关键注意

      • 预热执行:首次运行 GPU 程序需初始化,建议先执行 1 次核函数(不计时),再进行正式计时;

      • 区分“计算耗时”与“总耗时”:总耗时含数据传输时间,需分别测量对比。

      5. 完全并行示例:向量加法(第四章核心)

      并行逻辑

      • 每个线程处理 1 对向量元素的加法(数据并行),无分支、无依赖,完美适配 SIMD;

      • 线程配置:

        • 块内线程数(threadsPerBlock):通常设为 256/512/1024(需 ≤GPU`maxThreadsPerBlock`);

        • 网格大小(blocksPerGrid):(N + threadsPerBlock - 1) / threadsPerBlock,确保覆盖所有元素;

      • Guard Clause:if (i < N),因网格大小可能略大于元素数(如 N=10000,256 线程/块 →40 块 →10240 线程),避免线程索引越界。

      数据传输对性能的影响

      元素数量 CPU 耗时(ms) GPU 计算耗时(ms) GPU 总耗时(含传输,ms) 加速比(计算) 加速比(总耗时)

      10,000

      0.021

      0.089

      0.145

      0.24x

      0.15x

      100,000

      0.280

      0.123

      0.445

      2.27x

      0.63x

      1,000,000

      2.142

      0.134

      2.398

      15.91x

      0.89x

      10,000,000

      24.806

      0.577

      21.855

      42.95x

      1.14x

      100,000,000

      218.270

      4.955

      212.297

      44.04x

      1.02x

      • 结论:数据量越小,内存传输开销占比越高;数据量足够大时,GPU 计算优势才能抵消传输开销。

      6. 扩展:欧氏距离计算与性能分析

      • 核心公式:两点`(x1,y1,z1)(x2,y2,z2)的距离为√[(x1-x2)² + (y1-y2)² + (z1-z2)²]`;

      • 性能分析工具:NVIDIA Nsight Compute(ncu`命令),需在 Docker 启动时添加--cap-add=SYS_ADMIN`获取权限;

      • 分析指标:内存吞吐量、SM 利用率、缓存命中率等,用于定位性能瓶颈(如线程配置不合理、内存访问不连续)。

      二、C++(CUDA)示例代码

      示例 1:素数测试(GPU vs CPU,非完全并行)

      核心功能

      验证 GPU 多线程处理含循环/少量分支的任务,对比 CPU 串行执行效率,呼应第四章“非完全并行”示例。

      #include <cuda_runtime.h>
      #include <iostream>
      #include <chrono>
      #include <cmath>
      
      using namespace std;
      using namespace chrono;
      
      // 错误检查宏
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              cerr << "CUDA错误:" << cudaGetErrorString(err) << "(行号:" << __LINE__ << ")" << endl; \
              exit(1); \
          }
      
      // CPU串行素数测试
      bool checkPrimeCPU(long long num) {
          if (num <= 1) return false;
          if (num = 2) return true;
          if (num % 2 = 0) return false;
          for (long long i = 3; i * i <= num; i += 2) {
              if (num % i = 0) return false;
          }
          return true;
      }
      
      // GPU核函数:素数测试(每个线程处理一个奇数)
      __global__ void checkPrimeGPU(long long start, long long end, bool* d_results) {
          // 计算全局线程ID
          int tid = threadIdx.x + blockIdx.x * blockDim.x;
          // 映射到待测试数字(仅处理奇数,跳过偶数)
          long long num = start + (tid * 2);
          // 超出范围则退出
          if (num > end) return;
      
          bool isPrime = true;
          if (num <= 1) {
              isPrime = false;
          } else if (num = 2) {
              isPrime = true;
          } else if (num % 2 = 0) {
              isPrime = false;
          } else {
              for (long long i = 3; i * i <= num; i += 2) {
                  if (num % i = 0) {
                      isPrime = false;
                      break;
                  }
              }
          }
      
          // 存储结果到设备内存(线程ID对应结果索引)
          d_results[tid] = isPrime;
      }
      
      int main() {
          // 测试范围(100,001 ~ 190,001,仅奇数)
          const long long start = 100001;
          const long long end = 190001;
          const int total_nums = (end - start + 1) / 2;  // 奇数个数
      
          // 1. CPU串行测试
          auto cpu_start = high_resolution_clock::now();
          int cpu_prime_count = 0;
          for (long long num = start; num <= end; num += 2) {
              if (checkPrimeCPU(num)) cpu_prime_count++;
          }
          auto cpu_time = duration_cast<microseconds>(high_resolution_clock::now() - cpu_start).count() / 1000.0;
          cout << "CPU测试结果:" << cpu_prime_count << " 个素数,耗时:" << cpu_time << " ms" << endl;
      
          // 2. GPU并行测试
          // 线程配置
          const int threadsPerBlock = 256;
          const int blocksPerGrid = (total_nums + threadsPerBlock - 1) / threadsPerBlock;
      
          // 分配设备内存(存储结果)
          bool* d_results;
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_results, total_nums * sizeof(bool)));
      
          // 分配主机内存(接收结果)
          bool* h_results = new bool[total_nums];
      
          // GPU计时
          cudaEvent_t gpu_start, gpu_stop;
          CHECK_CUDA_ERR(cudaEventCreate(&gpu_start));
          CHECK_CUDA_ERR(cudaEventCreate(&gpu_stop));
          CHECK_CUDA_ERR(cudaEventRecord(gpu_start, 0));
      
          // 启动核函数(预热执行,不计入时间)
          checkPrimeGPU<<<blocksPerGrid, threadsPerBlock>>>(start, end, d_results);
          CHECK_CUDA_ERR(cudaDeviceSynchronize());
      
          // 正式执行并计时
          CHECK_CUDA_ERR(cudaEventRecord(gpu_start, 0));
          checkPrimeGPU<<<blocksPerGrid, threadsPerBlock>>>(start, end, d_results);
          CHECK_CUDA_ERR(cudaGetLastError());  // 检查核函数启动错误
          CHECK_CUDA_ERR(cudaEventRecord(gpu_stop, 0));
          CHECK_CUDA_ERR(cudaEventSynchronize(gpu_stop));
      
          // 计算GPU耗时
          float gpu_time;
          CHECK_CUDA_ERR(cudaEventElapsedTime(&gpu_time, gpu_start, gpu_stop));
      
          // 设备→主机拷贝结果
          CHECK_CUDA_ERR(cudaMemcpy(h_results, d_results, total_nums * sizeof(bool), cudaMemcpyDeviceToHost));
      
          // 统计GPU素数个数
          int gpu_prime_count = 0;
          for (int i = 0; i < total_nums; ++i) {
              if (h_results[i]) gpu_prime_count++;
          }
      
          cout << "GPU测试结果:" << gpu_prime_count << " 个素数,耗时:" << gpu_time << " ms" << endl;
          cout << "GPU相对CPU加速比:" << cpu_time / gpu_time << "x" << endl;
      
          // 释放资源
          delete[] h_results;
          CHECK_CUDA_ERR(cudaFree(d_results));
          CHECK_CUDA_ERR(cudaEventDestroy(gpu_start));
          CHECK_CUDA_ERR(cudaEventDestroy(gpu_stop));
      
          return 0;
      }

      编译与运行

      • 本地环境:nvcc prime_test.cu -o prime_test -std=c++11 && ./prime_test

      • Docker 环境:docker run -it --rm --gpus all -v ./:/code nvidia/cuda:12.0.0-devel-ubuntu20.04 bash,容器内执行上述编译运行命令;

      • 预期输出

        CPU测试结果:7648 个素数,耗时:15.61 ms
        GPU测试结果:7648 个素数,耗时:0.23 ms
        GPU相对CPU加速比:67.87x

      示例 2:向量加法(完全并行,含内存传输影响)

      核心功能

      实现 GPU 并行向量加法,对比“仅计算耗时”与“含内存传输总耗时”,呼应第四章“内存传输主导性能”的关键结论。

      #include <cuda_runtime.h>
      #include <iostream>
      #include <vector>
      #include <chrono>
      
      using namespace std;
      using namespace chrono;
      
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              cerr << "CUDA错误:" << cudaGetErrorString(err) << "(行号:" << __LINE__ << ")" << endl; \
              exit(1); \
          }
      
      // 块大小(适配GPU硬件,≤1024)
      const int THREADS_PER_BLOCK = 256;
      
      // CPU串行向量加法
      void vectorAddCPU(const float* h_A, const float* h_B, float* h_C, int N) {
          for (int i = 0; i < N; ++i) {
              h_C[i] = h_A[i] + h_B[i];
          }
      }
      
      // GPU核函数:向量加法(完全并行)
      __global__ void vectorAddGPU(const float* d_A, const float* d_B, float* d_C, int N) {
          // 全局线程ID(映射到向量索引)
          int i = threadIdx.x + blockIdx.x * blockDim.x;
          // Guard Clause:避免索引越界
          if (i < N) {
              d_C[i] = d_A[i] + d_B[i];
          }
      }
      
      int main() {
          // 向量大小(可修改为10000/100000/10000000/100000000)
          const int N = 10'000'000;
          const size_t data_size = N * sizeof(float);
      
          // 1. 主机内存分配与初始化
          vector<float> h_A(N, 1.23f);  // 向量A:全1.23
          vector<float> h_B(N, 4.56f);  // 向量B:全4.56
          vector<float> h_C_CPU(N, 0.0f);  // CPU结果
          vector<float> h_C_GPU(N, 0.0f);  // GPU结果
      
          // 2. CPU串行计算
          auto cpu_start = high_resolution_clock::now();
          vectorAddCPU(h_A.data(), h_B.data(), h_C_CPU.data(), N);
          auto cpu_time = duration_cast<milliseconds>(high_resolution_clock::now() - cpu_start).count();
          cout << "CPU串行计算耗时:" << cpu_time << " ms" << endl;
      
          // 3. GPU并行计算(含内存传输)
          float *d_A, *d_B, *d_C;
          // 分配设备内存
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_A, data_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_B, data_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_C, data_size));
      
          // 计时:总耗时(传输+计算+回传)
          auto gpu_total_start = high_resolution_clock::now();
      
          // 主机→设备:拷贝输入向量
          CHECK_CUDA_ERR(cudaMemcpy(d_A, h_A.data(), data_size, cudaMemcpyHostToDevice));
          CHECK_CUDA_ERR(cudaMemcpy(d_B, h_B.data(), data_size, cudaMemcpyHostToDevice));
      
          // 线程配置
          const int blocksPerGrid = (N + THREADS_PER_BLOCK - 1) / THREADS_PER_BLOCK;
      
          // GPU计算计时(仅计算)
          cudaEvent_t compute_start, compute_stop;
          CHECK_CUDA_ERR(cudaEventCreate(&compute_start));
          CHECK_CUDA_ERR(cudaEventCreate(&compute_stop));
          CHECK_CUDA_ERR(cudaEventRecord(compute_start, 0));
      
          // 启动核函数(预热)
          vectorAddGPU<<<blocksPerGrid, THREADS_PER_BLOCK>>>(d_A, d_B, d_C, N);
          CHECK_CUDA_ERR(cudaDeviceSynchronize());
      
          // 正式计算
          vectorAddGPU<<<blocksPerGrid, THREADS_PER_BLOCK>>>(d_A, d_B, d_C, N);
          CHECK_CUDA_ERR(cudaGetLastError());
          CHECK_CUDA_ERR(cudaEventRecord(compute_stop, 0));
          CHECK_CUDA_ERR(cudaEventSynchronize(compute_stop));
      
          // 计算仅计算耗时
          float compute_time;
          CHECK_CUDA_ERR(cudaEventElapsedTime(&compute_time, compute_start, compute_stop));
      
          // 设备→主机:拷贝结果
          CHECK_CUDA_ERR(cudaMemcpy(h_C_GPU.data(), d_C, data_size, cudaMemcpyDeviceToHost));
      
          // 总耗时结束
          auto gpu_total_time = duration_cast<milliseconds>(high_resolution_clock::now() - gpu_total_start).count();
      
          // 4. 结果验证(前10个元素)
          bool valid = true;
          for (int i = 0; i < 10; ++i) {
              if (abs(h_C_CPU[i] - h_C_GPU[i]) > 1e-5) {
                  valid = false;
                  break;
              }
          }
      
          // 输出结果
          cout << "GPU仅计算耗时:" << compute_time << " ms" << endl;
          cout << "GPU总耗时(含传输):" << gpu_total_time << " ms" << endl;
          cout << "结果验证:" << (valid ? "正确" : "错误") << endl;
          cout << "GPU计算加速比:" << (double)cpu_time / compute_time << "x" << endl;
          cout << "GPU总耗时加速比:" << (double)cpu_time / gpu_total_time << "x" << endl;
      
          // 5. 释放资源
          CHECK_CUDA_ERR(cudaFree(d_A));
          CHECK_CUDA_ERR(cudaFree(d_B));
          CHECK_CUDA_ERR(cudaFree(d_C));
          CHECK_CUDA_ERR(cudaEventDestroy(compute_start));
          CHECK_CUDA_ERR(cudaEventDestroy(compute_stop));
      
          return 0;
      }

      编译与运行

      • 编译命令:nvcc vector_add_full.cu -o vector_add_full -std=c++11

      • 运行命令:./vector_add_full

      • 预期输出(N=1000 万):

        CPU串行计算耗时:24.8 ms
        GPU仅计算耗时:0.58 ms
        GPU总耗时(含传输):21.9 ms
        结果验证:正确
        GPU计算加速比:42.76x
        GPU总耗时加速比:1.13x

      示例 3:欧氏距离计算(含 Nsight 性能分析)

      核心功能

      计算两组 3D 点之间的欧氏距离,演示 GPU 处理复杂数学运算,并用 Nsight Compute 分析性能。

      #include <cuda_runtime.h>
      #include <iostream>
      #include <vector>
      #include <chrono>
      #include <cmath>
      
      using namespace std;
      using namespace chrono;
      
      #define CHECK_CUDA_ERR(err) \
          if (err != cudaSuccess) { \
              cerr << "CUDA错误:" << cudaGetErrorString(err) << "(行号:" << __LINE__ << ")" << endl; \
              exit(1); \
          }
      
      // 3D点结构体
      struct Point {
          float x, y, z;
      };
      
      // GPU核函数:计算两组点的欧氏距离
      __global__ void calculateEuclideanDistanceGPU(Point* d_lineA, Point* d_lineB, float* d_distances, int numPoints) {
          int idx = threadIdx.x + blockIdx.x * blockDim.x;
          if (idx < numPoints) {
              float dx = d_lineA[idx].x - d_lineB[idx].x;
              float dy = d_lineA[idx].y - d_lineB[idx].y;
              float dz = d_lineA[idx].z - d_lineB[idx].z;
              // 用sqrtf(GPU优化的单精度平方根函数)
              d_distances[idx] = sqrtf(dx*dx + dy*dy + dz*dz);
          }
      }
      
      int main() {
          const int numPoints = 1'000'000;  // 100万个点
          const size_t point_size = numPoints * sizeof(Point);
          const size_t dist_size = numPoints * sizeof(float);
      
          // 1. 主机内存初始化
          vector<Point> h_lineA(numPoints);
          vector<Point> h_lineB(numPoints);
          vector<float> h_distances(numPoints, 0.0f);
      
          // 随机生成点坐标(0~100)
          srand(time(0));
          for (int i = 0; i < numPoints; ++i) {
              h_lineA[i] = {rand()%100, rand()%100, rand()%100};
              h_lineB[i] = {rand()%100, rand()%100, rand()%100};
          }
      
          // 2. 设备内存分配
          Point *d_lineA, *d_lineB;
          float *d_distances;
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_lineA, point_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_lineB, point_size));
          CHECK_CUDA_ERR(cudaMalloc((void**)&d_distances, dist_size));
      
          // 3. 数据拷贝与计算
          CHECK_CUDA_ERR(cudaMemcpy(d_lineA, h_lineA.data(), point_size, cudaMemcpyHostToDevice));
          CHECK_CUDA_ERR(cudaMemcpy(d_lineB, h_lineB.data(), point_size, cudaMemcpyHostToDevice));
      
          // 线程配置
          const int threadsPerBlock = 256;
          const int blocksPerGrid = (numPoints + threadsPerBlock - 1) / threadsPerBlock;
      
          // GPU计时
          cudaEvent_t start, stop;
          CHECK_CUDA_ERR(cudaEventCreate(&start));
          CHECK_CUDA_ERR(cudaEventCreate(&stop));
          CHECK_CUDA_ERR(cudaEventRecord(start, 0));
      
          calculateEuclideanDistanceGPU<<<blocksPerGrid, threadsPerBlock>>>(d_lineA, d_lineB, d_distances, numPoints);
          CHECK_CUDA_ERR(cudaGetLastError());
          CHECK_CUDA_ERR(cudaEventRecord(stop, 0));
          CHECK_CUDA_ERR(cudaEventSynchronize(stop));
      
          // 4. 结果回传与耗时计算
          float gpu_time;
          CHECK_CUDA_ERR(cudaEventElapsedTime(&gpu_time, start, stop));
          CHECK_CUDA_ERR(cudaMemcpy(h_distances.data(), d_distances, dist_size, cudaMemcpyDeviceToHost));
      
          cout << "GPU计算100万个3D点欧氏距离耗时:" << gpu_time << " ms" << endl;
          cout << "前5个距离结果:";
          for (int i = 0; i < 5; ++i) {
              cout << h_distances[i] << " ";
          }
          cout << endl;
      
          // 5. 性能分析(Docker环境)
          // 提示:在Docker中启动时添加--cap-add=SYS_ADMIN,执行:ncu -o distance_report ./euclidean_distance
          cout << "性能分析命令(Docker):ncu -o distance_report ./euclidean_distance" << endl;
      
          // 6. 释放资源
          CHECK_CUDA_ERR(cudaFree(d_lineA));
          CHECK_CUDA_ERR(cudaFree(d_lineB));
          CHECK_CUDA_ERR(cudaFree(d_distances));
          CHECK_CUDA_ERR(cudaEventDestroy(start));
          CHECK_CUDA_ERR(cudaEventDestroy(stop));
      
          return 0;
      }

      编译与性能分析

      • 编译命令:nvcc euclidean_distance.cu -o euclidean_distance -std=c++11

      • Docker 性能分析启动命令:docker run -it --rm --gpus all --cap-add=SYS_ADMIN -v ./:/code nvidia/cuda:12.0.0-devel-ubuntu20.04 bash

      • 容器内性能分析:ncu -o distance_report ./euclidean_distance

      • 导入分析报告:ncu --import distance_report.ncu-rep

      • 预期输出

        GPU计算100万个3D点欧氏距离耗时:0.32 ms
        前5个距离结果:22.3607 43.0116 15.2315 37.4166 52.915
        性能分析命令(Docker):ncu -o distance_report ./euclidean_distance

      三、关键说明

      1. 代码与知识点关联

      • 示例 1(素数测试):体现“非完全并行”场景,含循环/分支的 GPU 优化,验证线程 ID 映射与结果存储;

      • 示例 2(向量加法):核心演示“完全并行”与内存传输影响,Guard Clause 的必要性,以及“一次传输、多次计算”的优化思路;

      • 示例 3(欧氏距离):扩展复杂数学运算(平方、平方根),结合 Nsight Compute 性能分析,定位 GPU 利用率瓶颈。

      2. 常见问题排查

      • 内存泄漏:确保`cudaMalloc`分配的设备内存都用`cudaFree`释放,主机内存用`delete`/`vector`自动管理;

      • 核函数无结果:忘记`cudaDeviceSynchronize()或`cudaEventSynchronize(),导致 CPU 提前读取未计算完成的结果;

      • 索引越界:未加 Guard Clause,或线程配置错误(`blocksPerGrid`计算错误);

      • 性能分析失败:Docker 启动时未加`--cap-add=SYS_ADMIN`,导致`ncu`无权限访问 GPU 信息。

      3. 优化建议

      • 减少内存传输:将多次计算的输入数据一次性传输到 GPU,结果集中回传;

      • 线程配置优化:块内线程数设为 32 的倍数(适配 GPU 线程束大小),避免小于 32 或大于 1024;

      • 函数选型:使用 GPU 优化的数学函数(如`sqrtf`替代`sqrt`,sinf`替代`sin),提升计算效率。