CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第五节
第五节:了解和使用共享内存(2)
Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员。他在多个国家级的实验室进行大型并行运算的研究,并且是几个新创企业的合伙人。大家可以发邮件到rmfarber@gmail.com与他沟通和交流。
在CUDA系列文章的第四节里,我探讨了执行模型和内核启动执行配置是如何影响寄存器的数量和本地多处理器资源如共享内存的数量的。在本小节,我会继续探讨内存性能,和在reverseArray_multiblock_fast.cu.内使用共享内存。
CUDA 内存性能
本地内存空间和全局内存空间不会缓存,这意味着每次对全局内存(或本地内存)进行访问都将导致一次实际的内存访问。那么访问(例如读取或写入)各种类型的内存的“开销”是多少?
多处理器需要四(4)个时钟周期才能为每次“warp(交换)”发出一条内存指令。读取本地或全局内存。访问本地或全局内存将导致400到600个时钟周期的延迟。举例说明,在以下代码片段中的赋值运算符需要4个时钟周期才能从全局内存中进行一次读取,4个时钟周期从共享内存进行一次写入,需要400到600个时钟周期从全局内存读取一个浮点值。注意:4个时钟周期从共享内存进行一次写入,需要400到600个时钟周期从全局内存读取一个浮点值。注意:使用__device__变量类型限定符表示全局内存中的变量(有关其他变量特征,请参见CUDA Programming Guide第4.2.2.1节)。主代码不能访问变量类型__device__。
__shared__ float shared[]; __device__ float device[]; shared[threadIdx.x] = device[threadIdx.x];
当访问时间出现100-150倍差别时,难怪程序开发人员会需要最小化对全局内存的访问,并且在本地多处理器内存内重新使用数据。CUDA设计者对线程调度程序的设计十分巧妙,大量的全局内存延迟都可以透明地隐藏起来:只需在执行配置中指定大量数据块,并尽可能在内核中使用寄存器、__shared__和__constant__存储器类型处理变量即可。
因为共享内存在芯片上,因此访问速度要比读取全局内存快很多,并且主要的优化是为了避免存储器组冲突。共享内存速度较快(有些文章认为它和寄存器访问一样快捷)。
然而,最近CUBLAS和CUFFT性能获得极大的改进:通过尽量使用共享内存而非寄存器-因此如可能的话,尽量使用寄存器吧。CUDA共享内存被分为大小相同的内存模块,称为存储器组(memorybank)。每个存储器组都保存有一个连续的32位值(如int和float),因此通过连续线程对连续数组访问非常的快捷。当向同一个存储数组(可能是同一个地址或映射到同一个存储数组的多个地址)发出数据请求时,存储器组发生冲突。如果发生这种情况,硬件会有效地序列化内存运算,强迫所有线程等待直到内存请求得以完成。如果所有的线程从同一个共享内存地址读取,那就会自动调用广播机制,避免序列化。共享内存广播是一个同时向多个线程提供数据的有效方法。使用共享内存时,您完全可以尝试下利用这一特点。
我将在下一个专栏文章中更为详细地讨论存储器组冲突。现在,只需要知道reverseArray_multiblock_fast.cu 没有存储器冲突,因为连续线程方位连续值。
具有读取/写入功能的多处理器本地存储器类型总结如下:
寄存器
- 多处理器上最快的内存形式;
- 仅可通过线程访问;
- 有线程的生命周期
共享内存
- 在没有存储器组冲突(从同一个地址读取)时与寄存器一样快。
- 可从创建线程的任何块访问;
- 有线程的生命周期
全局内存:
- 可能比寄存器或共享内存慢150倍,注意非联合读取和写入(将在下一专栏中讨论)。
- 可从主机或设备访问;
- 有应用程序的生命周期
本地内存:
- 潜在的性能缺陷,位于全局内存中,可能比寄存器或共享内存慢150倍。
- 仅可通过线程访问;
- 有线程的生命周期
共享内存注意事项
小心共享内存存储器冲突,可能会导致性能降低;
所有在内核里的动态分配的共享变量在同一个内存地址开始。使用至少两个动态分配共享内存数组要求手动生成偏移量。例如,如果你想动态分配共享内存,以包含两个数组, a 和b, 你需要进行如下操作:
__global__ void kernel(int aSize)
{
extern __shared__ float sData[];
float *a, *b;
a = sData;
b = &a[aSize]; __global__ void kernel(int aSize)
{
extern __shared__ float sData[];
float *a, *b;
a = sData;
b = &a[aSize];
寄存器/本地内存注意事项
寄存器内存可以透明地存入本地内存。这可能会造成性能欠佳。检查ptx 汇编代码或在通过nvcc在输出中查找lmem,命令如下:“—ptxas-options=-v”。
编译时已经知道的通过常量索引的数组通常存于寄存器中,但是如果使用变量索引,它们就不能存于寄存器中。这就为开发人员弄出了一个难题,因为可能需要循环展开以在寄存器内存,而非较慢的全局内存中,保存数组元素。然而,展开循环会急剧增加寄存器使用量,因而就可能会使得变量被保存在本地内存中――从而抵消了循环展开的诸多好处。可以使用使用nvcc选项,“—maxrregcount=value”告诉编译器使用更多寄存器(注意:可以指定的最大寄存器数量为128)。这需要在“使用更多的寄存器”和“创建更少的线程”之间权衡利弊,有可能会妨碍隐藏存储器延迟。在某些架构中,使用该选项可能造成资源不足,从而导致内核无法启动。
共享内存内核
程序reverseArray_multiblock.cu和revereseArray_multiblock_fast.cu 执行同样的任务。它们创建一个一维整数数组h_a,数组包含整数值[0 ..dimA-1]。数组可以通过cudaMemcpy移动到设备,然后主机启动reverseArrayBlock内核就地逆转数组内容的顺序。再次使用cudaMemcpy将数据从设备传回主机,并执行一个检查,检查设备生成了正确的结果(例如[dimA-1 .. 0])。
区别就是:reverseArray_multiblock_fast.cu使用共享内存以改进内核的性能,而reverseArray_multiblock.cu在全局内存里执行。尝试下给这两个程序计时,验证下它们的性能区别。另外,reverseArray_multiblock.cu 访问全局内存的效率不高。在以后的专栏文章里,我们将使用CUDAPROFILER协助诊断和纠正这个性能,并且展示最新的10个系列架构中的改进如何减少了许多情况下对这些优化的需求。
#include <stdio.h>
#include <assert.h>
#include "cuda.h"
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <device_functions.h>
//检查CUDA运行时是否有错误
void checkCUDAError(const char* msg);
// Part 2 of 2: 使用共享内存执行内核
__global__ void reverseArrayBlock(int *d_out, int *d_in)
{
extern __shared__ int s_data[];
int inOffset = blockDim.x * blockIdx.x;
int in = inOffset + threadIdx.x;
// Load one element per thread from device memory and store it
// *in reversed order* into temporary shared memory
/*
每个线程从设备内存加载一个数据元素并按逆序存储在共享存储器上
*/
s_data[blockDim.x - - threadIdx.x] = d_in[in];
/*
阻塞,一直到所有线程将他们的数据都写入到共享内存中
*/
__syncthreads();
// write the data from shared memory in forward order,
// but to the reversed block offset as before
/*
将共享内存中的数据s_data写入到d_out中,按照前序
*/
int outOffset = blockDim.x * (gridDim.x - - blockIdx.x);
int out = outOffset + threadIdx.x;
d_out[out] = s_data[threadIdx.x];
}
////////////////////////////////////////////////////////////////////
//主函数
////////////////////////////////////////////////////////////////////
int main(int argc, char** argv)
{
//指向主机的内存空间和大小
int *h_a;
int dimA = * ; // 256K elements (1MB total)
// pointer for device memory
int *d_b, *d_a;
//指向设备的指针和大小
int numThreadsPerBlock = ; /*
根据数组大小和预设的块大小来计算需要的块数
*/
int numBlocks = dimA / numThreadsPerBlock;
/*
Part 1 of 2:
计算共享内存所需的内存空间大小,这在下面的内核调用时被使用
*/
int sharedMemSize = numThreadsPerBlock * sizeof(int);
//申请主机及设备上的存储空间
size_t memSize = numBlocks * numThreadsPerBlock * sizeof(int);
//主机上的大小
h_a = (int *)malloc(memSize);
//设备上的大小
cudaMalloc((void **)&d_a, memSize);
cudaMalloc((void **)&d_b, memSize);
//在主机上初始化输入数组
for (int i = ; i < dimA; ++i)
{
h_a[i] = i;
}
//将主机数组拷贝到设备上,h_a-->d_a
cudaMemcpy(d_a, h_a, memSize, cudaMemcpyHostToDevice);
//启动内核
dim3 dimGrid(numBlocks);
dim3 dimBlock(numThreadsPerBlock);
reverseArrayBlock << < dimGrid, dimBlock, sharedMemSize >> >(d_b, d_a);
//阻塞,一直到设备完成计算
cudaThreadSynchronize();
//检查是否设备产生了错误
//检查任何CUDA错误
checkCUDAError("kernel invocation");
//将结果从设备拷贝到主机,d_b-->h_a
cudaMemcpy(h_a, d_b, memSize, cudaMemcpyDeviceToHost);
//检查任何CUDA错误
checkCUDAError("memcpy");
//核对返回到主机上的结果是否正确
for (int i = ; i < dimA; i++)
{
assert(h_a[i] == dimA - - i);
}
//释放设备内存
cudaFree(d_a);
cudaFree(d_b);
//释放主机内存
free(h_a);
printf("Correct!\n");
return ;
} void checkCUDAError(const char *msg)
{
cudaError_t err = cudaGetLastError();
if (cudaSuccess != err)
{
fprintf(stderr, "Cuda error: %s: %s.\n", msg, cudaGetErrorString(err));
exit(EXIT_FAILURE);
}
}
确定运行时共享内存的量需要在主机和设备代码里进行设置。在本示例中,内核中每个块的共享内存的量(字节为单位)在主机上的执行配置中予以明确,作为可选的第三个参数。(仅当共享内存的容量在内核启动时指定后,才设置主机端。如果在编译时修复,则主机端不需要任何设置。)例如,在主机代码arrayReversal_multiblock_fast.cu中,以下代码片段为一个整数数组(包含的元素数等于数据块中的线程数)分配共享内存:
// Part 1 of 2: Compute the number of bytes of share memory needed
// This is used in the kernel invocation below
int sharedMemSize = numThreadsPerBlock * sizeof(int);
检测reverseArrayBlock 内核, 共享内存使用以下代码声明:
extern __shared__ int s_data[]; extern __shared__ int s_data[];
注意:内核中没有指定大小-大小通过执行配置从主机获取。
在下一个关于配置的专栏前,我建议研究一下reverseArray_multiblock.cu。您认为访问全局内存时存在性能问题吗?如果您认为存在,请尝试解决这一问题。
CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第五节的更多相关文章
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第七节
第七节:使用下一代CUDA硬件,快乐加速度 原文链接 Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多个 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第六节
原文链接 第六节:全局内存和CUDA RPOFILER Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第四节
了解和使用共享内存(1) Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多个国家级的实验室进行大型并行运 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第十节
原文链接 第十节:CUDPP, 强大的数据平行CUDA库Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第一节
原文链接 第一节 CUDA 让你可以一边使用熟悉的编程概念,一边开发可在GPU上运行的软件. Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Lab ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第八节
原文链接 第八节:利用CUDA函数库 Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多个国家级的实验室进 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第二节
原文链接 第二节:第一个内核 Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多个国家级的实验室进行大型并 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第九节
原文链接 第九节:使用CUDA拓展高等级语言 Rob Farber 是西北太平洋国家实验室(Pacific Northwest National Laboratory)的高级科研人员.他在多个国家级的 ...
- CUDA:Supercomputing for the Masses (用于大量数据的超级计算)-第三节
原文链接 第三节:错误处理和全局内存性能局限 恭喜!通过对CUDA(Compute Unified DeviceArchitecture,即计算统一设备架构的首字母缩写)系列文章第一节和第二节,您现在 ...
随机推荐
- Linux 常用命令一览
本篇博文讲述系统内核.Bash解释器的关系与作用,如何正确的执行Linux命令以及常见排错方法. 经验丰富的运维人员可以恰当的组合命令与参数,使Linux字符命令更加的灵活且相对减少消耗系统资源. 强 ...
- 开发外包注意事项二——iOS APP的开发
目前我的方式是按时间算. 首先这得建立在双方的信任基础上. 以我做过的Case为例: 首先会和客户一起评估需求: 1. 哪些功能是最为重要的 2. 哪些功能是可以删除的 3. 用什么策略保证APP的出 ...
- [SCOI2007]修车 费用流 BZOJ 1070
题目描述 同一时刻有N位车主带着他们的爱车来到了汽车维修中心.维修中心共有M位技术人员,不同的技术人员对不同的车进行维修所用的时间是不同的.现在需要安排这M位技术人员所维修的车及顺序,使得顾客平均等待 ...
- PAT天梯赛L2-007 家庭房产
题目链接:点击打开链接 给定每个人的家庭成员和其自己名下的房产,请你统计出每个家庭的人口数.人均房产面积及房产套数. 输入格式: 输入第一行给出一个正整数N(<=1000),随后N行,每行按下列 ...
- JMeter - 连续性能测试 - JMeter + ANT + Jenkins集成 - 第2部分
目标: 创建包含性能测试流程的持续交付管道,以尽早检测任何与性能相关的问题. 通常,全面的性能测试将在分段/预生产环境中完成,该环境可能与您的生产环境相同.在完成QA功能/回归验证后,将代码推送到分段 ...
- 使用navicat把一个数据库的表导入到另外一个数据库
第一步:右击数据库名,选择数据传输 第二步:全选要导的数据库表 第三步:选择目标中的数据库,然后开始就可以了
- jetty-env.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE Configure PUBLIC &quo ...
- 使用 dbutils 的结果集包装类 StringTrimmedResultSet
1.功能 StringTrimmedResultSet 的功能是去掉结果集中数据的前后空格,这个方法是在取结果的时候处理. 2.使用 一般在新建 QueryRunner 对象的时候使用: QueryR ...
- WebP图片格式
腾讯科技讯 科技博客GigaOM近日撰文称,谷歌(微博)试图让WebP图片格式取代JPEG等现有图片格式.虽然谷歌无法很快达成所愿,但WebP仍然会对互联网产生重大影响. 文章全文如下: 受够了 ...
- 关于jetty的那些奇葩问题
Jetty的解压目录并不像Tomcat那样直接是在webapps下,如果你什么都不做修改的话,Ubuntu14.04下Jetty的默认解压目录是/var/cache/jetty/data/下: 比如我 ...