如果要在核函数内做比较大规模一点的数组拷贝,应该怎么做呢?
最简单的想法自然是每个线程拷贝对应的下标
cpp
__global__ void globalCopy(const float* data, float* out,const int N) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < N) {
// 直接读全局内存
out[idx] = data[idx];
}
}
这并没有什么问题,但是是否能够加速呢?
使用共享内存作缓存
令人反直觉的想法是,可以先把源数组的内容放到共享内存,再由共享内存拷贝到目的数组。
cpp
//假如一个block只启动一个warp(32线程)
__global__ void SharedMemoryCopy(const float* data, float* out,const int N){
__shared__ float SharedMemory[32];
int idx=threadIdx.x+blockIdx.x*blockDim.x;
if(threadIdx.x < 32 &&idx<N){
SharedMemory[threadIdx.x]=data[idx];
}
__syncthreads();
if(threadIdx.x < 32 &&idx<N){
out[idx]=SharedMemory[threadIdx.x];
}
}
向量化拷贝
如果拷贝的时候相邻的内容会一起拷贝,可以考虑向量化拷贝
cpp
__global__ void VectorCopy(float* data, float* out,const int N){
using Vec=float4;//float4有4个float数据
const int vec_size=4;
const int vec_num=N/vec_size;//这里默认N是4的倍数,不然得作尾部处理
int idx=threadIdx.x+blockIdx.x*blockDim.x;
if(idx<vec_num){
Vec *vec_src_ptr=reinterpret_cast<Vec*>(data)+idx;
Vec *vec_dst_ptr=reinterpret_cast<Vec*>(out)+idx;
*vec_dst_ptr=*vec_src_ptr;
}
}
测试
cpp
#include <cuda_runtime.h>
#include <iostream>
#include <vector>
__global__ void globalCopy(const float* data, float* out,const int N) {
int idx = threadIdx.x + blockIdx.x * blockDim.x;
if (idx < N) {
// 直接读全局内存
out[idx] = data[idx];
}
}
//假如一个block只启动一个warp(32线程)
__global__ void SharedMemoryCopy(const float* data, float* out,const int N){
__shared__ float SharedMemory[32];
int idx=threadIdx.x+blockIdx.x*blockDim.x;
if(threadIdx.x < 32 &&idx<N){
SharedMemory[idx]=data[idx];
}
__syncthreads();
if(threadIdx.x < 32 &&idx<N){
out[idx]=SharedMemory[idx];
}
}
__global__ void VectorCopy(float* data, float* out,const int N){
using Vec=float4;
const int vec_size=4;
const int vec_num=N/vec_size;
int idx=threadIdx.x+blockIdx.x*blockDim.x;
if(idx<vec_num){
Vec *vec_src_ptr=reinterpret_cast<Vec*>(data)+idx;
Vec *vec_dst_ptr=reinterpret_cast<Vec*>(out)+idx;
*vec_dst_ptr=*vec_src_ptr;
}
}
int main() {
cudaEvent_t start,end;
float SharedMemoryCopyTime=0,globalCopyTime=0,VectorCopyTime=0;
cudaEventCreate(&start);
cudaEventCreate(&end);
float host_data[32];
std::fill_n(host_data,32,1.0f);
float *d_data,*d_out;
cudaMalloc(&d_data,32*sizeof(float));
cudaMalloc(&d_out,32*sizeof(float));
cudaMemcpy(d_data,host_data,32*sizeof(float),cudaMemcpyHostToDevice);
cudaEventRecord(start);
globalCopy<<<1,32>>>(d_data,d_out,32);
cudaEventRecord(end);
cudaEventSynchronize(end);
cudaEventElapsedTime(&globalCopyTime,start,end);
cudaEventRecord(start);
SharedMemoryCopy<<<1,32>>>(d_data,d_out,32);
cudaEventRecord(end);
cudaEventSynchronize(end);
cudaEventElapsedTime(&SharedMemoryCopyTime,start,end);
cudaEventRecord(start);
VectorCopy<<<1,32>>>(d_data,d_out,32);
cudaEventRecord(end);
cudaEventSynchronize(end);
cudaEventElapsedTime(&VectorCopyTime,start,end);
std::cout<<"globalCopy的时间是: "<<globalCopyTime<<std::endl;
std::cout<<"SharedMemoryCopy的时间是: "<<SharedMemoryCopyTime<<std::endl;
std::cout<<"VectorCopy的时间是: "<<VectorCopyTime<<std::endl;
cudaFree(d_data);
cudaFree(d_out);
cudaEventDestroy(start);
cudaEventDestroy(end);
}
bash
globalCopy的时间是: 138.551
SharedMemoryCopy的时间是: 36.309
VectorCopy的时间是: 36.5588
(单位是ms)
为什么?
全局内存访问(globalCopy)
-
每次
out[idx] = data[idx];
都是 全局内存读取 + 写回 -
GPU 全局内存延迟大约 400--600 cycles
-
即使 L1/L2 缓存能命中,也比共享内存慢很多
-
所以时间最长:138 ms(实验结果)
使用共享内存(SharedMemoryCopy)
-
步骤:
-
SharedMemory[idx] = data[idx];
→ 读全局内存到共享内存 -
out[idx] = SharedMemory[idx];
→ 从共享内存写回全局内存
-
-
关键点:
-
虽然多了一次写入共享内存,但共享内存访问非常快(几 cycles)
-
如果多个线程访问同一个 warp 或相邻地址,共享内存是 完全 coalesced 的寄存器级别速度
-
第一次全局内存读的 latency 被 warp 内隐藏(大量线程可以 overlap)
-
-
因此,整体来看,比直接每次全局内存读写快很多
VectorCopy(float4)
-
将 float 拆成
float4
,一次读写 4 个 float -
优点:
-
全局内存访问 coalesced,一次 transaction 传 16 bytes
-
warp 内存访问效率极高
-
核心原因总结
-
全局内存慢 → globalCopy 最慢
-
共享内存快 + warp 重叠 → 虽然多了拷贝,但每个线程对全局内存访问少了一次,速度提升
-
Vectorized load → float4 减少 memory transaction 数量,也接近共享内存速度
💡 简单理解:
GPU 的瓶颈不是"计算",而是 全局内存访问延迟和带宽 。把数据先放共享内存,相当于 一次全局内存读,多次快速访问共享内存,整体就快了。
把向量化和共享内存结合呢?
cpp
//假如一个block只启动一个warp(32线程)
__global__ void VectorSharedMemoryCopy(float* data, float* out,const int N){
using Vec=float4;
__shared__ Vec SharedMemory[8];
const int vec_size=4;
const int vec_num=N/vec_size;
int idx=threadIdx.x+blockIdx.x*blockDim.x;
if(threadIdx.x < 8 &&idx<N){
SharedMemory[threadIdx.x]=*(reinterpret_cast<Vec*>(data)+idx);
}
__syncthreads();
if(threadIdx.x < 8 &&idx<N){
*(reinterpret_cast<Vec*>(out)+idx)=SharedMemory[threadIdx.x];
}
}
//启动
VectorSharedMemoryCopy<<<1,32>>>(d_data,d_out,32);
bash
VectorSharedMemoryCopy的时间是: 35.1693
没啥明显提升,可能的原因是数据量太小了