CUDA编程(五)关注内存的存取模式

CUDA编程(五)

关注内存的存取模式

上一篇博客我们使用Thread完成了简单的并行加速,虽然我们的程序运行速度有了50甚至上百倍的提升,但是根据内存带宽来评估的话我们的程序还远远不够,

除了通过Block继续提高线程数量来优化性能,这次想给大家先介绍一个访存方面非常重要的优化,同样可以大幅提高程序的性能~

什么样的存取模式是高效的?

大家知道一般显卡上的内存是 DRAM,因此最有效率的存取方式,是以连续的方式存取,单纯说连续存取可能比较抽象,我们还是通过例子来看这个问题。

之前的程序,大家可以看到我们非常重要的核函数部分:

// __global__ 函数 (GPU上执行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{

    //表示目前的 thread 是第几个 thread(由 0 开始计算)
    const int tid = threadIdx.x;

    //计算每个线程需要完成的量
    const int size = DATA_SIZE / THREAD_NUM;

    int sum = 0;

    int i;

    //记录运算开始的时间
    clock_t start;

    //只在 thread 0(即 threadIdx.x = 0 的时候)进行记录
    if (tid == 0) start = clock();

    for (i = tid * size; i < (tid + 1) * size; i++) {

        sum += num[i] * num[i] * num[i];

    }

    result[tid] = sum;

    //计算时间的动作,只在 thread 0(即 threadIdx.x = 0 的时候)进行
    if (tid == 0) *time = clock() - start;

}

在计算立方和的部分,虽然看起来是连续存取内存位置(每个 thread 对一块连续的数字计算平方和),但是实际上并不是这样的,我们要考虑到实际上 thread 的执行方式。

前面提过,当一个 thread 在等待内存的数据时,GPU 会切换到下一个 thread。也就是说,实际上线程执行的顺序是类似

thread 0 -> thread 1 -> thread 2 -> thread 3 -> thread 4 ->...

因此,在同一个 thread 中连续存取内存,在实际执行时反而不是连续了,下图很明显的反应了这个问题,我们的存取是跳跃式的。

要让实际执行结果是连续的存取,我们应该要让 thread 0 读取第一个数字,thread 1 读取第二个数字…依此类推,很容易可以想象,通过这种存储方式,我们取数字的时候就变成了连续存取。

改进存取模式

根据我们上面的分析,我们原本的核函数并不是连续存取的,读取数字完全是跳跃式的读取,这会非常影响内存的存取效率,因此我们下一步要将取数字的过程变成:

thread 0 读取第一个数字,thread 1 读取第二个数字…

这点通过对核函数的for循环进行一个小修改就可以达到了~

// __global__ 函数 (GPU上执行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{

    //表示目前的 thread 是第几个 thread(由 0 开始计算)
    const int tid = threadIdx.x;

    int sum = 0;

    int i;

    //记录运算开始的时间
    clock_t start;

    //只在 thread 0(即 threadIdx.x = 0 的时候)进行记录
    if (tid == 0) start = clock();

    //改为连续存取(thread 0 读取第一个数字,thread 1 读取第二个数字 …)
    for (i = tid; i < DATA_SIZE; i += THREAD_NUM) {

        sum += num[i] * num[i] * num[i];

    }

    result[tid] = sum;

    //计算时间的动作,只在 thread 0(即 threadIdx.x = 0 的时候)进行
    if (tid == 0) *time = clock() - start;

}

通过上面对for循环的一个小修改就可以达到目的了,那么这么一个微小的修改到底有多大作用呢?

完整代码 :

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

//CUDA RunTime API
#include <cuda_runtime.h>

//1M
#define DATA_SIZE 1048576

#define THREAD_NUM 1024

int data[DATA_SIZE];

//产生大量0-9之间的随机数
void GenerateNumbers(int *number, int size)
{
    for (int i = 0; i < size; i++) {
        number[i] = rand() % 10;
    }
}

//打印设备信息
void printDeviceProp(const cudaDeviceProp &prop)
{
    printf("Device Name : %s.\n", prop.name);
    printf("totalGlobalMem : %d.\n", prop.totalGlobalMem);
    printf("sharedMemPerBlock : %d.\n", prop.sharedMemPerBlock);
    printf("regsPerBlock : %d.\n", prop.regsPerBlock);
    printf("warpSize : %d.\n", prop.warpSize);
    printf("memPitch : %d.\n", prop.memPitch);
    printf("maxThreadsPerBlock : %d.\n", prop.maxThreadsPerBlock);
    printf("maxThreadsDim[0 - 2] : %d %d %d.\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
    printf("maxGridSize[0 - 2] : %d %d %d.\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
    printf("totalConstMem : %d.\n", prop.totalConstMem);
    printf("major.minor : %d.%d.\n", prop.major, prop.minor);
    printf("clockRate : %d.\n", prop.clockRate);
    printf("textureAlignment : %d.\n", prop.textureAlignment);
    printf("deviceOverlap : %d.\n", prop.deviceOverlap);
    printf("multiProcessorCount : %d.\n", prop.multiProcessorCount);
}

//CUDA 初始化
bool InitCUDA()
{
    int count;

    //取得支持Cuda的装置的数目
    cudaGetDeviceCount(&count);

    if (count == 0) {
        fprintf(stderr, "There is no device.\n");
        return false;
    }

    int i;

    for (i = 0; i < count; i++) {

        cudaDeviceProp prop;
        cudaGetDeviceProperties(&prop, i);
        //打印设备信息
        printDeviceProp(prop);

        if (cudaGetDeviceProperties(&prop, i) == cudaSuccess) {
            if (prop.major >= 1) {
                break;
            }
        }
    }

    if (i == count) {
        fprintf(stderr, "There is no device supporting CUDA 1.x.\n");
        return false;
    }

    cudaSetDevice(i);

    return true;
}

// __global__ 函数 (GPU上执行) 计算立方和
__global__ static void sumOfSquares(int *num, int* result, clock_t* time)
{

    //表示目前的 thread 是第几个 thread(由 0 开始计算)
    const int tid = threadIdx.x;

    int sum = 0;

    int i;

    //记录运算开始的时间
    clock_t start;

    //只在 thread 0(即 threadIdx.x = 0 的时候)进行记录
    if (tid == 0) start = clock();

    for (i = tid; i < DATA_SIZE; i += THREAD_NUM) {

        sum += num[i] * num[i] * num[i];

    }

    result[tid] = sum;

    //计算时间的动作,只在 thread 0(即 threadIdx.x = 0 的时候)进行
    if (tid == 0) *time = clock() - start;

}

int main()
{

    //CUDA 初始化
    if (!InitCUDA()) {
        return 0;
    }

    //生成随机数
    GenerateNumbers(data, DATA_SIZE);

    /*把数据复制到显卡内存中*/
    int* gpudata, *result;

    clock_t* time;

    //cudaMalloc 取得一块显卡内存 ( 其中result用来存储计算结果,time用来存储运行时间 )
    cudaMalloc((void**)&gpudata, sizeof(int)* DATA_SIZE);
    cudaMalloc((void**)&result, sizeof(int)*THREAD_NUM);
    cudaMalloc((void**)&time, sizeof(clock_t));

    //cudaMemcpy 将产生的随机数复制到显卡内存中
    //cudaMemcpyHostToDevice - 从内存复制到显卡内存
    //cudaMemcpyDeviceToHost - 从显卡内存复制到内存
    cudaMemcpy(gpudata, data, sizeof(int)* DATA_SIZE, cudaMemcpyHostToDevice);

    // 在CUDA 中执行函数 语法:函数名称<<<block 数目, thread 数目, shared memory 大小>>>(参数...);
    sumOfSquares << < 1, THREAD_NUM, 0 >> >(gpudata, result, time);

    /*把结果从显示芯片复制回主内存*/

    int sum[THREAD_NUM];

    clock_t time_use;

    //cudaMemcpy 将结果从显存中复制回内存
    cudaMemcpy(&sum, result, sizeof(int)* THREAD_NUM, cudaMemcpyDeviceToHost);
    cudaMemcpy(&time_use, time, sizeof(clock_t), cudaMemcpyDeviceToHost);

    //Free
    cudaFree(gpudata);
    cudaFree(result);
    cudaFree(time);

    int final_sum = 0;

    for (int i = 0; i < THREAD_NUM; i++) {

        final_sum += sum[i];

    }

    printf("GPUsum: %d  gputime: %d\n", final_sum, time_use);

    final_sum = 0;

    for (int i = 0; i < DATA_SIZE; i++) {

        final_sum += data[i] * data[i] * data[i];

    }

    printf("CPUsum: %d \n", final_sum);

    return 0;
}

运行结果 :

我们看到这次运行用了894297个时钟周期

不知道大家是否还记得上次我们用1024个线程运行的最终结果:6489302个时钟周期,现在我们只是很简单的改了一下存取模式,同样使用1024个线程最终只使用了894297个时钟周期

6489302/894297= 7.26

可以看到我们的速度居然提升了7.26倍,而我们只是单纯修改了一下存取模式罢了,所以我们可以看到连续存取这个存取优化还是十分重要的,在我们没法再单纯地从线程数量上继续优化的情况下,从存取模式上进行的这个优化是十分有效的。

我们还是从内存带宽的角度来进行一下评估:

首先计算一下使用的时间:

894297/ (797000 * 1000) =  0.0011221S

然后计算使用的带宽:

数据量仍然没有变 DATA_SIZE 1048576,也就是1024*1024 也就是 1M

1M 个 32 bits 数字的数据量是 4MB。

因此,这个程序实际上使用的内存带宽约为:

4MB / 0.0011221S = 3564.745MB/s = 3.48GB/s

注意我们没进行内存存取优化之前的内存带宽是491MB/s,可以看到,我们通过这个优化一下子就把内存带宽提升到了GB级别,不得不说这是一个非常令人满意的效果,我们在没有继续增加线程数量的情况下,通过把内存的存取模式变成连续的,取得了7倍左右的加速。

总结:

这篇博客主要讲解了通过如何尽可能的连续操作内存,减少内存存取方面的时间浪费。

通过最终的结果我们可以看到,看似不起眼的一个小改进(尽可能的去连续操作内存),竟然有这近7倍的性能提升,所以希望大家记住这个优化,在优化我们的CUDA程序的时候,一定不要忘记从内存存取角度去进行一些优化,这往往能取得出乎意料的结果。

希望我的博客能帮助到大家~

参考资料:《深入浅出谈CUDA》

时间: 2024-10-12 16:56:21

CUDA编程(五)关注内存的存取模式的相关文章

Ubuntu12.04 之 CUDA 编程 (二) ~~~ GPU 程序加速

关于 Ubuntu12.04 下 CUDA5.5 的安装请参看如下链接 Ubuntu-12.04 安装 CUDA-5.5 关于 Ubuntu12.04 下 CUDA5.5 程序的运行请参看如下链接 Ubuntu12.04 之 CUDA 编程 (一) --- GPU 运行程序 1.程序的并行化 前一篇文章讲到了如何利用 CUDA5.5 在 GPU 中运行一个程序.通过程序的运行,我们看到了 GPU 确实可以作为一个运算器,但是,我们在前面的例子中并没有正真的发挥 GPU 并行处理程序的能力,也就是

详解CUDA编程

CUDA 是 NVIDIA 的 GPGPU 模型,它使用 C 语言为基础,可以直接以大多数人熟悉的 C 语言,写出在显示芯片上执行的程序,而不需要去学习特定的显示芯片的指令或是特殊的结构." 编者注:NVIDIA的GeFoce 8800GTX发布后,它的通用计算架构CUDA经过一年多的推广后,现在已经在有相当多的论文发表,在商业应用软件等方面也初步出现了视频编解码.金融.地质勘探.科学计算等领域的产品,是时候让我们对其作更深一步的了解.为了让大家更容易了解CUDA,我们征得Hotball的本人同

CUDA C编程入门-编程接口(3.5)模式转换

GPUs有一个显示输出,输出到一个叫主表面的DRAM内存中,这个表面被用于刷新输出给用户看的显示设备.当用户通过改变显示器的分辨率或者位深(使用NVIDIA控制面版或者Windows显示器控制面版),开始一个显示器模式选择时,主表面需要的内存数量会改变.例如,如果以后改变显示器分辨率从1280x1024x32位到1600x1200x32位,系统必须增加7.68MB而不是5.24MB的内存给主表面(带反走样的全屏的图形应用可能要求更多的显示内存给主表面).在Windows上,其它事件,包括加载一个

cuda编程:关于共享内存(shared memory)和存储体(bank)的事实和疑惑

关于共享内存(shared memory)和存储体(bank)的事实和疑惑 主要是在研究访问共享内存会产生bank conflict时,自己产生的疑惑.对于这点疑惑,网上都没有相关描述, 不管是国内还是国外的网上资料.貌似大家都是当作一个事实,一个公理,而没有对其仔细研究.还是我自己才学疏浅,不知道某些知识. 比如下面这篇讲解bank conflict的文章. http://cuda-programming.blogspot.com/2013/02/bank-conflicts-in-share

Linux网络编程 五种I/O 模式及select、epoll方法的理解

Linux网络编程 五种I/O 模式及select.epoll方法的理解 web优化必须了解的原理之I/o的五种模型和web的三种工作模式 五种I/O 模式--阻塞(默认IO模式),非阻塞(常用语管道),I/O多路复用(IO多路复用的应用场景),信号I/O,异步I/OLinux网络编程 五种I/O 模式及select.epoll方法的理解

CUDA编程之快速入门

CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架构.做图像视觉领域的同学多多少少都会接触到CUDA,毕竟要做性能速度优化,CUDA是个很重要的工具,CUDA是做视觉的同学难以绕过的一个坑,必须踩一踩才踏实.CUDA编程真的是入门容易精通难,具有计算机体系结构和C语言编程知识储备的同学上手CUDA编程应该难度不会很大.本文章将通过以下五个方面帮助大家比较全面地了解CUDA编程最重要的知识点,做到快速入门: GPU架构特点 CUDA线程模型

CUDA编程之快速入门【转】

https://www.cnblogs.com/skyfsm/p/9673960.html CUDA(Compute Unified Device Architecture)的中文全称为计算统一设备架构.做图像视觉领域的同学多多少少都会接触到CUDA,毕竟要做性能速度优化,CUDA是个很重要的工具,CUDA是做视觉的同学难以绕过的一个坑,必须踩一踩才踏实.CUDA编程真的是入门容易精通难,具有计算机体系结构和C语言编程知识储备的同学上手CUDA编程应该难度不会很大.本文章将通过以下五个方面帮助大

CUDA编程常见问题 转

http://blog.csdn.net/yutianzuijin/article/details/8147912 分类: 编程语言2012-11-05 10:55 2521人阅读 评论(0) 收藏 举报 cudaGPU 最近初试cuda编程,作为一个新手,遇到了各种各样的问题,然后花费了大量时间解决这些匪夷所思的问题.为了避免后来人重蹈覆辙,现把自己遇到的问题总结如下. (一).cudaMalloc 初次使用该函数,感觉没有什么困难,和c语言的malloc类似.但是在具体应用中却出了一个很难找

设计模式(十五):Iterator迭代器模式 -- 行为型模式

1.概述 类中的面向对象编程封装应用逻辑.类,就是实例化的对象,每个单独的对象都有一个特定的身份和状态.单独的对象是一种组织代码的有用方法,但通常你会处理一组对象或者集合. 集合不一定是均一的.图形用户界面框架中的 Window 对象可以收集任意数量的控制对象 - Menu.Slider 和 Button.并且,集合的实现可以有多种方式:PHP 数字是一个集合,但也是一个散列表,一个链接列表,一个堆栈以及队列. 例子1:电视遥控器的频道遍历 2.问题 如何操纵任意的对象集合? 如一个列表(Lis