GPU编程实战指南03:CUDA开发快速上手示例,GPU性能碾压实测

上一节《GPU编程指南02:CUDA开发快速上手示例》中我们完成了一个使用GPU进行加减乘除四则运算的例子。没有学习的可以先跳转学习这一节,因为它有详细的代码注释,学习完这一篇,你就基本入门了GPU编程。

在这个例子中,我们使用GPU进行运算,同时也会用CPU进行运算,然后将两者的结果进行对比,以确保我们代码运行的结果是正确的。

既然CPU可以计算,为什么要用GPU呢?因为GPU可以进行并行计算,计算效率高。为了验证这一点,我们将上节的代码进行完善,分别加入CPU和GPU运算的耗时统计,以直观的方式展示GPU强大的并行运算性能!

#include <stdio.h>
#include <stdlib.h>
#include <cuda_runtime.h>
#include <chrono> // 用于CPU时间测量

/***
 * CUDA 基础概念:
 * 1. 线程层次结构:
 *    - 线程(Thread):基本执行单元
 *    - 线程块(Block):由多个线程组成
 *    - 网格(Grid):由多个线程块组成
 *
 * 2. 重要内置变量:
 *    - threadIdx:线程在块内的索引
 *    - blockIdx:线程块在网格中的索引
 *    - blockDim:线程块的维度(每线程块的线程数)
 *    - gridDim:网格的维度(每网格线程块数量)
 ***/

// 定义数组大小
#define N 2000000000

// CUDA 核函数 - 加法
// __global__: 表示这是一个在GPU上运行并可以从CPU调用的核函数
__global__ void addKernel(const float* a, const float* b, float* c, int n) {
    // 计算全局线程索引:
    // blockIdx.x: 网格中当前块的索引
    // blockDim.x: 每个块的线程数
    // threadIdx.x: 块中当前线程的索引
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] + b[idx];
    }
}

// CUDA 核函数 - 减法
// 类似于加法,这个核函数在GPU上执行并行减法
__global__ void subtractKernel(const float* a, const float* b, float* c, int n) {
    // 计算全局线程索引
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] - b[idx];
    }
}

// CUDA 核函数 - 乘法
// 在GPU上执行并行乘法
__global__ void multiplyKernel(const float* a, const float* b, float* c, int n) {
    // 计算全局线程索引
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        c[idx] = a[idx] * b[idx];
    }
}

// CUDA 核函数 - 除法
// 在GPU上执行并行除法,包括处理除以零的情况
__global__ void divideKernel(const float* a, const float* b, float* c, int n) {
    // 计算全局线程索引
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    if (idx < n) {
        if (b[idx] != 0) { // 避免除以零
            c[idx] = a[idx] / b[idx];
        } else {
            c[idx] = 0; // 当除数为零时,将结果设为0
        }
    }
}

// 检查CUDA错误
// 该函数检查CUDA操作是否成功,失败时输出错误信息并退出
// CUDA API调用通常返回cudaError_t类型的错误码
void checkCudaError(cudaError_t error, const char* message) {
    if (error != cudaSuccess) {
        fprintf(stderr, "CUDA错误: %s - %s\n", message, cudaGetErrorString(error));
        exit(-1);
    }
}

// 用于结果验证的CPU计算函数版本
void cpuCalculate(const float* a, const float* b, float* c, int n, char operation) {
    for (int i = 0; i < n; i++) {
        switch (operation) {
            case '+':
                c[i] = a[i] + b[i];
                break;
            case '-':
                c[i] = a[i] - b[i];
                break;
            case '*':
                c[i] = a[i] * b[i];
                break;
            case '/':
                c[i] = (b[i] != 0) ? a[i] / b[i] : 0;
                break;
        }
    }
}

// 验证GPU和CPU结果
bool verifyResults(const float* cpu_result, const float* gpu_result, int n) {
    const float epsilon = 1e-5; // 允许的误差范围
    for (int i = 0; i < n; i++) {
        if (fabs(cpu_result[i] - gpu_result[i]) > epsilon) {
            printf("结果不匹配! 索引 %d: CPU = %f, GPU = %f\n", i, cpu_result[i], gpu_result[i]);
            return false;
        }
    }
    return true;
}

int main() {
    // 声明主机和设备内存指针
    // h_前缀表示主机内存(CPU内存)
    // d_前缀表示设备内存(GPU内存)
    float *h_a, *h_b, *h_c, *h_verify;
    float *d_a, *d_b, *d_c;
    
    // 声明CUDA事件变量用于GPU计时
    cudaEvent_t start_gpu, stop_gpu;
    float gpu_time_ms = 0.0f;
    
    // 分配主机内存(使用标准C的malloc函数)
    h_a = (float*)malloc(N * sizeof(float)); // 第一个输入数组
    h_b = (float*)malloc(N * sizeof(float)); // 第二个输入数组
    h_c = (float*)malloc(N * sizeof(float)); // 存储GPU计算结果
    h_verify = (float*)malloc(N * sizeof(float)); // 存储CPU验证结果

    // 初始化输入数据
    // 用随机数填充输入数组
    for (int i = 0; i < N; i++) {
        h_a[i] = rand() / (float)RAND_MAX; // 生成0到1之间的随机浮点数
        h_b[i] = rand() / (float)RAND_MAX;
    }

    // 分配设备内存(GPU内存)
    // cudaMalloc是用于GPU内存分配的CUDA API
    // 第一个参数是指向指针的指针,第二个是要分配的字节数
    checkCudaError(cudaMalloc((void**)&d_a, N * sizeof(float)), "分配设备内存d_a失败");
    checkCudaError(cudaMalloc((void**)&d_b, N * sizeof(float)), "分配设备内存d_b失败");
    checkCudaError(cudaMalloc((void**)&d_c, N * sizeof(float)), "分配设备内存d_c失败");

    // 将数据从主机内存复制到设备内存
    // cudaMemcpy是用于主机和设备之间数据传输的CUDA API
    // 参数:目标指针,源指针,要复制的字节数,复制方向
    // cudaMemcpyHostToDevice表示从CPU复制到GPU
    checkCudaError(cudaMemcpy(d_a, h_a, N * sizeof(float), cudaMemcpyHostToDevice), "复制数据到设备d_a失败");
    checkCudaError(cudaMemcpy(d_b, h_b, N * sizeof(float), cudaMemcpyHostToDevice), "复制数据到设备d_b失败");

    // 设置CUDA核函数启动参数
    // 定义块大小和网格大小
    int threadsPerBlock = 256; // 每个块包含256个线程
    // 计算处理所有数据所需的块数
    // 公式确保有足够的线程处理所有N个元素
    int blocksPerGrid = (N + threadsPerBlock - 1) / threadsPerBlock;

    // 执行加法运算
    printf("\n正在执行GPU加法运算...\n");
    
    // 创建CUDA事件用于GPU计时
    cudaEventCreate(&start_gpu);
    cudaEventCreate(&stop_gpu);
    
    // 开始GPU计时
    cudaEventRecord(start_gpu, 0);
    
    // 执行GPU加法运算
    addKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
    checkCudaError(cudaGetLastError(), "加法核函数启动失败");
    
    // 结束GPU计时
    cudaEventRecord(stop_gpu, 0);
    cudaEventSynchronize(stop_gpu);
    cudaEventElapsedTime(&gpu_time_ms, start_gpu, stop_gpu);
    
    // 复制结果回主机
    checkCudaError(cudaMemcpy(h_c, d_c, N * sizeof(float), cudaMemcpyDeviceToHost), "拷贝加法结果到主机失败");
    
    // 开始CPU计时
    auto start_cpu = std::chrono::high_resolution_clock::now();
    
    // 执行CPU加法运算
    cpuCalculate(h_a, h_b, h_verify, N, '+');
    
    // 结束CPU计时
    auto stop_cpu = std::chrono::high_resolution_clock::now();
    auto cpu_time_ms = std::chrono::duration<float, std::milli>(stop_cpu - start_cpu).count();
    
    // 计算加速比
    float speedup = cpu_time_ms / gpu_time_ms;
    
    // 输出结果
    printf("加法运算验证: %s\n", verifyResults(h_verify, h_c, N) ? "通过" : "未通过");
    printf("GPU执行时间: %.3f ms\n", gpu_time_ms);
    printf("CPU执行时间: %.3f ms\n", cpu_time_ms);
    printf("加速比: %.2f\n", speedup);

    // 执行减法运算
    printf("\n正在执行GPU减法运算...\n");
    
    // 重置CUDA事件计时器
    cudaEventRecord(start_gpu, 0);
    
    // 使用相同的网格和块配置启动减法核函数
    subtractKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
    checkCudaError(cudaGetLastError(), "减法核函数启动失败");
    
    // 结束GPU计时
    cudaEventRecord(stop_gpu, 0);
    cudaEventSynchronize(stop_gpu);
    cudaEventElapsedTime(&gpu_time_ms, start_gpu, stop_gpu);
    
    // 将GPU结果复制回CPU
    checkCudaError(cudaMemcpy(h_c, d_c, N * sizeof(float), cudaMemcpyDeviceToHost), "复制减法结果到主机失败");
    
    // 开始CPU计时
    start_cpu = std::chrono::high_resolution_clock::now();
    
    // 在CPU上执行相同的减法
    cpuCalculate(h_a, h_b, h_verify, N, '-');
    
    // 结束CPU计时
    stop_cpu = std::chrono::high_resolution_clock::now();
    cpu_time_ms = std::chrono::duration<float, std::milli>(stop_cpu - start_cpu).count();
    
    // 计算加速比
    speedup = cpu_time_ms / gpu_time_ms;
    
    // 验证结果
    printf("减法运算验证: %s\n", verifyResults(h_verify, h_c, N) ? "通过" : "未通过");
    printf("GPU执行时间: %.3f ms\n", gpu_time_ms);
    printf("CPU执行时间: %.3f ms\n", cpu_time_ms);
    printf("加速比: %.2f\n", speedup);

    // 执行乘法运算
    printf("\n正在执行GPU乘法运算...\n");
    
    // 重置CUDA事件计时器
    cudaEventRecord(start_gpu, 0);
    
    // 启动乘法核函数
    multiplyKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
    checkCudaError(cudaGetLastError(), "乘法核函数启动失败");
    
    // 结束GPU计时
    cudaEventRecord(stop_gpu, 0);
    cudaEventSynchronize(stop_gpu);
    cudaEventElapsedTime(&gpu_time_ms, start_gpu, stop_gpu);
    
    // 复制结果回主机
    checkCudaError(cudaMemcpy(h_c, d_c, N * sizeof(float), cudaMemcpyDeviceToHost), "复制乘法结果到主机失败");
    
    // 开始CPU计时
    start_cpu = std::chrono::high_resolution_clock::now();
    
    // 执行CPU乘法运算
    cpuCalculate(h_a, h_b, h_verify, N, '*');
    
    // 结束CPU计时
    stop_cpu = std::chrono::high_resolution_clock::now();
    cpu_time_ms = std::chrono::duration<float, std::milli>(stop_cpu - start_cpu).count();
    
    // 计算加速比
    speedup = cpu_time_ms / gpu_time_ms;
    
    // 输出结果
    printf("乘法运算验证: %s\n", verifyResults(h_verify, h_c, N) ? "通过" : "未通过");
    printf("GPU执行时间: %.3f ms\n", gpu_time_ms);
    printf("CPU执行时间: %.3f ms\n", cpu_time_ms);
    printf("加速比: %.2f\n", speedup);

    // 执行除法运算
    printf("\n正在执行GPU除法运算...\n");
    
    // 重置CUDA事件计时器
    cudaEventRecord(start_gpu, 0);
    
    // 启动除法核函数
    divideKernel<<<blocksPerGrid, threadsPerBlock>>>(d_a, d_b, d_c, N);
    checkCudaError(cudaGetLastError(), "除法核函数启动失败");
    
    // 结束GPU计时
    cudaEventRecord(stop_gpu, 0);
    cudaEventSynchronize(stop_gpu);
    cudaEventElapsedTime(&gpu_time_ms, start_gpu, stop_gpu);
    
    // 复制结果回主机
    checkCudaError(cudaMemcpy(h_c, d_c, N * sizeof(float), cudaMemcpyDeviceToHost), "复制除法结果到主机失败");
    
    // 开始CPU计时
    start_cpu = std::chrono::high_resolution_clock::now();
    
    // 执行CPU除法运算
    cpuCalculate(h_a, h_b, h_verify, N, '/');
    
    // 结束CPU计时
    stop_cpu = std::chrono::high_resolution_clock::now();
    cpu_time_ms = std::chrono::duration<float, std::milli>(stop_cpu - start_cpu).count();
    
    // 计算加速比
    speedup = cpu_time_ms / gpu_time_ms;
    
    // 输出结果
    printf("除法运算验证: %s\n", verifyResults(h_verify, h_c, N) ? "通过" : "未通过");
    printf("GPU执行时间: %.3f ms\n", gpu_time_ms);
    printf("CPU执行时间: %.3f ms\n", cpu_time_ms);
    printf("加速比: %.2f\n", speedup);

    // 销毁CUDA事件计时器
    cudaEventDestroy(start_gpu);
    cudaEventDestroy(stop_gpu);
    
    // 清理内存
    // 使用cudaFree释放GPU内存
    cudaFree(d_a);
    cudaFree(d_b);
    cudaFree(d_c);
    // 使用标准C的free释放CPU内存
    free(h_a);
    free(h_b);
    free(h_c);
    free(h_verify);

    return 0;
}

编译
在这里插入图片描述
运行
在这里插入图片描述
我们可以看到使用GPU效率要远高于CPU。其中加法运算的加速比达到了188!而其他三个也分别达到了10、6、13。
对这个结果我有些疑问,于是问问AI:

问题:从运行结果来看,执行加法运算使用GPU加速效果最好,加速比为188。而减法、乘法、除法加速比分别为10、6、13。请问这个结果合理吗?为什么不同的运算加速比会有如此大的差异?请给我详细而专业的解释。
下面是AI的回答:

GPU加速比差异分析

从您提供的运行结果来看,不同算术运算的GPU加速比有显著差异:

  • 加法:188倍
  • 减法:10倍
  • 乘法:6.倍
  • 除法:13倍

这种差异是合理的,主要有以下几个原因:

1. 指令复杂度差异

不同算术运算在硬件层面的复杂度不同:

  • 加法:是最简单的算术运算,在GPU和CPU上都很快,但GPU的并行优势使其加速比最高
  • 乘法:比加法复杂,需要更多的计算资源和时钟周期
  • 除法:是最复杂的基本算术运算,通常需要多个时钟周期

GPU对简单运算的并行加速效果更明显,因为简单运算更容易被大规模并行化处理。

2. 内存带宽限制

您的代码是典型的内存密集型操作:

global void addKernel(const float* a, const float* b, float* c, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}

对于加法这样的简单运算,性能主要受内存带宽限制,而不是计算能力限制。GPU的内存带宽远高于CPU,因此加法获得了最高的加速比。

3. 计算密度与内存访问比例

不同运算的计算密度(计算操作与内存访问的比例)不同:

  • 加法:计算密度低,每次只需一个简单操作
  • 乘法:计算密度较高,需要更多的计算资源
  • 除法:计算密度最高,但也最复杂

当计算密度低时,GPU的内存带宽优势占主导;当计算密度高时,计算能力的差异会部分抵消内存带宽的优势。

4. 硬件优化差异

现代GPU和CPU对不同运算有不同程度的硬件优化:

  • GPU通常针对单精度浮点加法有极高的优化
  • CPU可能对复杂运算(如除法)有特殊的硬件优化
  • 乘法在某些CPU架构上有专门的SIMD指令优化

5. 线程调度和同步开销

对于复杂运算,GPU线程执行时间差异可能更大,导致线程调度和同步开销增加,降低了并行效率。

结论

您观察到的加速比差异是合理的,反映了不同算术运算在GPU和CPU上的性能特性。加法运算获得最高加速比是因为它最能充分利用GPU的高内存带宽和大规模并行处理能力,而不受计算复杂度的限制。

如果您想进一步优化性能,可以考虑:

  1. 调整线程块大小以更好地适应您的GPU架构
  2. 使用共享内存减少全局内存访问
  3. 对于复杂运算,考虑使用更高效的算法或库函数

现在我们对CPU和GPU编程有了直观上的了解。下一节,我们尝试来优化GPU运算的性能,以达到更高的加速效果!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值