0001笔记【并行计算】CUDA在现代C++中如何运用?看这一个就够了
目录
SM(流多处理器)和板块(block)
一个板块会被调度到一个SM上,直到执行结束
- 一个block一旦被调度到某个SM上,就会一直执行,直到执行结束(gpu不存在时间片轮转),好处是不需要像cpu一样切换上下文,就也不需要保存寄存器和共享内存的开销
- 一个block里的一个线程,则是被调度到一个SP上执行
常用函数
cudaMalloc在显存上分配内存
cudaMallocHost在主存上分配锁页内存
- cudaMallocHost分配的是CPU内存,并且GPU可以直接读写,通过PCI-E总线传输(带宽约为6G/s)
- 注意要cudaDeviceSynchroinize()后再使用,不然数据可能错乱
cudaMemcpy在主存和显存之间拷贝数据
- 会隐含一次cpu和gpu同步(cudaDeviceSynchroinize())
- 要注意双工问题:Zero-copy can be full duplex, but cudaMemcpys are only half-duplex.
展开查看:零拷贝是全双工的,但是cudaMemcpys是半双工的(同一时间只能往一个方向运送数据)
cudaMallocManagerd统一内存
- 用cudaMallocManagerd分配的内存在cpu和gpu上都可以访问,
因为显卡驱动会自动的把数据在主存和显存之间来回拷贝 - 需要手动同步一下,才能保证数据正确(主动写cudaDeviceSynchroinize())
零拷贝Zero-Copy减少来回复制
- 零拷贝需要借助pinned-memory锁页内存,适用于只需要一次读取或者写入的数据操作(频繁读写不要用零拷贝)
展开查看:示例代码
``` float *a_h, *a_map; // 定义两个指针:a_h 内存原指针,a_map映射指针 cudaGetDeviceProperties(&prop, 0); // 获取GPU的特性,看是否支持地址映射 if (!prop.canMapHostMemory) exit(0); cudaSetDeviceFlags(cudaDeviceMapHost); // 设置设备属性,打开地址映射 cudaHostAlloc(&a_h, nBytes, cudaHostAllocMapped); // 开辟pinned memory cudaHostGetDevicePointer(&a_map, a_h, 0); // 地址映射 a_h -> a_map. kernel<<>>(a_map); ```
优化
时间依赖和空间依赖
- 时间依赖:假设有代码
int sum; for(i=0;i<100;i++) { sum += arr[i]; }
其中sum就是有时间依赖的,第10次循环里的sum的结果依赖于第9次循环的sum,实际上就变成了串行
如果编译器看到结果依赖于前一个值,就不会优化,导致效率不高
线程太多不行:防止寄存器打翻(register spill)
- 当kernel函数声明普通变量时,是存放再寄存器里的
展开查看:示例代码
__global__ void sum(int* arr) { int i; // 存放在寄存器里 }
- blockDim太大容易发生寄存器打翻:
- 一个block有固定数量的寄存器,寄存器会均匀的分给这个block中的所有线程,如果线程太多,每个线程分到的寄存器数量就很小
- 如果你每个线程声明的普通变量太多,寄存器存不下,就会使用L1缓存
线程太少不行:读数据时延很高
- "大吞吐量掩盖高时延"这里的高时延指的就是从显存DRAW读数据很慢,所以一个线程读取数据的时候就会切换到其他线程执行
- 结论:1.使用寄存器少但访存使用多的函数,尽可能多的开线程(让blockDim大)如:矢量加法。2. 光线追踪尽可能少开线程(让blockDim小,但不要太小)
减少跨步才高效
- 例如矩阵转置,同时存在按行访问[1]和按列访问[2],无论如何都存在跨步,只能把访存跨步转移为共享内存跨步,效率才会有所提升
展开查看:共享内存快,所以把跨步转移到共享内存上能提高一点效率
共享内存反直觉优化
bank和__shared__Memory概念
sinf和__sinf(两个下划线)的区别
- sinf精度比__sinf高,但__sinf计算更快
- 当n接近1e25时,__sinf完全错误
thrust(gpu的仿STL库)
Gpu原子操作
问题:
-
有线程就可以实现并行了,那为什么要引入板块(grid和block)的概念?
答:- gpu编程常常涉及三维和二维矩阵运算,引入grid和block会比较方便(有x,y,z)
- 如果没有block和grid线程就都是一维的,就不方便
- 矩阵转置的例子:如果只有一维,会发生按行访问和按列访问,导致低效。cuda有三维天生带循环分块
展开查看:循环分块截图02:36:03
-
声明和定义放在同一个文件内,真的可以提高性能吗?
答:教程00:35:34里提到,放在同一个文件内编译器可能知道要如何优化,如果分离了声明和定义,编译器会生成call指令,从而导致效率降低 -
"Gpu是使用大吞吐量掩盖延迟"这句话如何理解?
答:- 高延迟:指的是gpu读取显存很慢,如下图,从GlobleMemory(DRAM)中读取数据是很慢的
- 大吞吐量:线程足够多,当一个线程在读取数据的时候,就可以切换到其他线程进行运算
展开查看:Gpu内存模型图截图
-
主存与显存分配函数区分
- 主机内存:malloc, free
- 设备内存: cudaMalloc, cudaFree
- 统一内存(managerd):cudaMallocManaged, cudaFree
统一内存虽然方便,但毕竟有一点开销,尽可能用主机内存和设备内存吧
-
在向量加法中,如果向量大小不能被32整除怎么办?
- 课程00:58::36中说可以用网格跨步循环
展开查看:
- 向上取整,(n+blockDim-1) / blockDim这条式子可以多申请一个block,然后用if判断threadId有没有超过向量长度即可
展开查看:截图中红线部分错了,应该是减一
- 课程00:58::36中说可以用网格跨步循环
-
cpu函数向gpu函数传参数要传值还是传引用?
- 传值,因为要考虑是gpu内存还是cpu内存
- 用lambda捕获外部vector时,最好是捕获指针vec.data(),防止发生拷贝影响性能
展开查看:lambda捕获外部vec最好用vec.data()
-
如何测量gpu耗时?
- 要多测几次,先让gpu预热(显卡驱动初始化也耗时间)再测试更准
- 用nsight工具测时间最准
-
课程中提到的"再线程局部分配一个数组,并通过动态下标访问(例如遍历BVH树时用到的模拟栈),是无论如何都会打翻到一级缓存的,因为寄存器不能动态寻址"是啥?
展开查看:截图
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了