起因

人们对于性能的渴望不过是因为急躁
不能耐心等待结果而产生的

提升性能,有且仅有两种方式

  1. 提升硬件(博主还不会这个…………)
  2. 改进算法

今天我们讲讲另一种奇行种:
改进算法来利用硬件…………

CUDA 是什么?

维基百科上是这么介绍的:

1
2
3
CUDA (or Compute Unified Device Architecture) is a parallel computing platform and application programming interface (API) that allows software to use certain types of graphics processing unit (GPU) for general purpose processing – an approach called general-purpose computing on GPUs (GPGPU). CUDA is a software layer that gives direct access to the GPU's virtual instruction set and parallel computational elements, for the execution of compute kernels.
CUDA is designed to work with programming languages such as C, C++, and Fortran. This accessibility makes it easier for specialists in parallel programming to use GPU resources, in contrast to prior APIs like Direct3D and OpenGL, which required advanced skills in graphics programming. CUDA-powered GPUs also support programming frameworks such as OpenMP, OpenACC and OpenCL; and HIP by compiling such code to CUDA.
CUDA was created by Nvidia. When it was first introduced, the name was an acronym for Compute Unified Device Architecture, but Nvidia later dropped the common use of the acronym.

中文版(谷歌翻译):

1
2
3
CUDA(或Compute Unified Device Architecture)是一种并行计算平台和应用程序编程接口(API),它允许软件使用某些类型的图形处理单元(GPU)进行通用处理——这种方法称为GPU上的通用计算(GPGPU) )。 CUDA 是一个软件层,可以直接访问 GPU 的虚拟指令集和并行计算元素,以执行计算内核。 
CUDA 旨在使用 C、C++ 和 Fortran 等编程语言。这种可访问性使并行编程专家可以更轻松地使用 GPU 资源,这与 Direct3D 和 OpenGL 等需要高级图形编程技能的先前 API 形成鲜明对比。 CUDA 驱动的 GPU 还支持 OpenMP、OpenACC 和 OpenCL 等编程框架; 和 HIP 通过将此类代码编译为 CUDA。
CUDA 是由 Nvidia 创建的。 首次引入时,该名称是 Compute Unified Device Architecture 的首字母缩写词, 但 Nvidia 后来放弃了该首字母缩写词的常见用法。

说白了,它是利用GPU上众多的微小处理器来将大任务拆解成小任务解决的技术
计算总量是不变的,但是物理时长可以减少
尽管这种比喻并不可取,但是你可以想象成你的GPU上有千万个小小的处理器(CPU),命令让每个小处理器去分别执行

CUDA 应该怎么用?

CUDA 做不到的

尽管CUDA可以尽可能地利用你的显卡
但是它所能完成的任务类型必须符合以下条件:

  1. 可以拆解成众多原子操作
  2. 每个原子操作之间不讲究先后关系
  3. 原子操作不应该是复杂的
    为什么这么说呢?
    首先,如果你的算法不能拆解成原子操作,例如选择排序
    那么这相当于把GPU当作CPU来用了,每个操作都需要遍历一遍数组
    其次,如果你的原子操作之间讲究先后关系的话
    那么GPU所带来的并行性就体现不出来,反而增加了在主机与设备之间拷贝数据的时间
    最后,为什么要求原子操作不应该是复杂的
    因为CPU上的小处理器并不能像你的CPU那样完成许多复杂的操作

一定要用 CUDA 做的

比方说对数组每个元素的处理,
例如你要把一个数组里面所有元素的值乘以2
传统的做法是遍历整个数组,对每个元素乘以2
这样做的时间复杂度应当是ʘ(n)
而使用 CUDA 则是把原始数据拷贝到显卡的显存上
然后 GPU 上众多小处理器每个小处理器分别计算一个元素的两倍
这样就把时间复杂度降到了ʘ(1),假设不考虑拷贝数据的花销
再比方说对图形的处理,计算机中的图像可以当作一个二位数组来处理,
例如你要把一张图像蒙上一层滤镜,同时假设图像是正方形的,且大小(像素)为 n*n
传统做法是命令 CPU 遍历整个二维数组,对每个像素点进行颜色的操作
而利用 GPU ,则可以让每个核分别处理一个像素点
这样就把 ʘ(n²) 的操作变成了 ʘ(1)
想想吧,为什么打游戏渲染图形需要显卡
这才是根本原因吧

第一个 CUDA 项目

先决条件

  1. 良好的 C++ 基础
  2. 基础的 内存 模型理解
  3. 一张 NVIDIA RTX 显卡
  4. Visual Studio 2019 (工作承载要求勾选:使用C++的桌面开发)
  5. CUDA 开发工具包
    所有上述资源在网络上普遍存在,唯一要注意的是
    你需要先安装 VS 2019,再安装 CUDA 的 开发SDK。

Hello CUDA!

  1. 打开 Visual Studio
  2. 进入 File | New | Project
  3. 依次选择 NVIDIA | CUDA 9.0 | CUDA 9.0 Runtime
  4. 为项目填写名称,然后单击 OK 按钮
  5. 它将创建一个带有 kernel.cu 示例项目的文件
  6. 然后就可以开始编码了

演示 Demo

这个 Demo 对比了用 CPU 和 GPU 来分别计算两个等长随机元素的数组对应元素相加用时
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <time.h>
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
const int maxn = 1000000; // 元素值上限 - 一百万
const int maxl = 1000000; // 数组长 - 一百万
int* h_a, * h_b, * h_sum, * h_cpusum, tot = 0; // 数据指针
clock_t pro_start, pro_end, sum_start, sum_end; // CPU 时钟计数
cudaEvent_t e_start, e_stop; // CUDA 性能测试点
__global__ void numAdd(int* a, int* b, int* sum) {
int i = threadIdx.x + blockIdx.x * blockDim.x;
sum[i] = a[i] + b[i];
}
int main() {
srand(time(0)); // 设定随机数种子
h_a = new int[maxl], h_b = new int[maxl], h_sum = new int[maxl], h_cpusum = new int[maxl]; // 动态分配内存
pro_start = clock();
for (int i = 0; i < maxl; ++i) {
h_a[i] = rand() % maxn; h_b[i] = rand() % maxn; // 初始化随机数据
}
pro_end = clock();
// 输出 生成随机数 用时 (由于 CUDA 上无法完成随机数的初始化,遂不进行对比)
printf("produce %d random number to two array use time : %0.3f ms\n", maxl, double(pro_end - pro_start) / CLOCKS_PER_SEC * 1000);
system("pause");
int* d_a, * d_b, * d_sum;
cudaEventCreate(&e_start); cudaEventCreate(&e_stop); // 创建 CUDA 事件
cudaEventRecord(e_start, 0); // 记录 CUDA 事件
// 在显存上分配单元
cudaMalloc((void**)&d_a, sizeof(int) * maxl);
cudaMalloc((void**)&d_b, sizeof(int) * maxl);
cudaMalloc((void**)&d_sum, sizeof(int) * maxl);
/*printf("memory allocated!\n");*/
// 拷贝内存数据到显存
cudaMemcpy(d_a, h_a, sizeof(int) * maxl, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, sizeof(int) * maxl, cudaMemcpyHostToDevice);
/*printf("copy finished!\n");*/
numAdd << <1000, 1000 >> > (d_a, d_b, d_sum); // 调用 Kernel 函数传入参数并执行代码
cudaDeviceSynchronize(); // 同步GPU上的所有线程,等待所有线程结束后再继续
/*printf("sum finished!\ncopy backing...\n");*/
cudaMemcpy(h_sum, d_sum, sizeof(int) * maxl, cudaMemcpyDeviceToHost); // 从显存拷贝结果数据到内存
/*printf("copy backed!\n");*/
cudaFree(d_a); cudaFree(d_b); cudaFree(d_sum); // 释放GPU上分配的显存
/*printf("device RAM released!\n");*/
cudaEventRecord(e_stop, 0); cudaEventSynchronize(e_stop); // 记录用时
float elapsedTime;
cudaEventElapsedTime(&elapsedTime, e_start, e_stop);
printf("CUDA sum use time : %0.3f ms\n", elapsedTime);
sum_start = clock();
for (int i = 0; i < maxl; ++i) h_cpusum[i] = h_a[i] + h_b[i]; // CPU 进行加法计算
sum_end = clock();
printf("CPU sum use time : %0.3f ms\n", double(pro_end - pro_start) / CLOCKS_PER_SEC * 1000);
for (int i = 0; i < maxl; ++i) if (h_cpusum[i] != h_sum[i]) ++tot; // 检验 CPU 计算结果 与 GPU 计算结果是否一致,统计不一致个数
printf("error sum num : %d\n", tot);
system("pause");
delete h_a; delete h_b; delete h_sum; // 释放内存
return 0;
}

博主使用的机器是 i9-9900k 以及 Nvidia RTX 2070
最后运行性能表现如下:

1
2
3
4
5
6
produce 1000000 random number to two array use time : 69.000 ms
请按任意键继续. . .
CUDA sum use time : 3.444 ms
CPU sum use time : 69.000 ms
error sum num : 0
请按任意键继续. . .

那么在这台机器上
GPU 性能比 CPU 足足高出 20倍
那么有同学可能就要问了, 说好 ʘ(n) 变 ʘ(1) 的呢?
是这样的,上述代码在测量 GPU 性能时包含了数据从内存经总线传输到显存上的用时
而且,测量 CPU 用时使用的是 CPU 时钟计,这个是不准确的,严格来说,CPU 用时应该更长

总结

  1. CUDA 可以加速一些简单但量大的操作
  2. CUDA 适合处理图像
  3. CUDA 程序的标准工作流程是
    1. 分配显存单元
    2. 从内存拷贝数据到显存
    3. 通知显卡开始计算
    4. 拷贝结果数据回主机
    5. 使用结果数据

另外小小吐槽一下:博主同时安装了 VS 2022 和 VS 2019,因为 Nvidia 目前还没有将 CUDA 的开发包扩展迁移到 VS 2022 上,博主不得不又安装了 VS 2019
那么问题是,在 VS 2019 和 VS 2022 中设置主题是冲突的
例如, 博主的 VS 2022 使用的 NightOwl 主题, 当我设置 VS 2019 为深色主题时, VS 2022 会变成默认的蓝色主题
探究后发现,NightOwl 主题也是基于深色主题的,当两个版本的 IDE 设置为同一个根主题时,主题就会冲突…………
就,挺影响的🤣