菜鸟学opencl(1)两个数组相加

    最近由于工作需要所以开始学习opencl,由于刚毕业不久所以只有c语言,c#的基础,写博文主要是给大部分和我一样准备学习opencl的菜鸟参考。

    我的显卡是AMD公司的,所以先从AMD Opencl入手。

    看了一些教程,因为都是从英文直接翻译过来的,感觉理解上有些费力,我觉得用图片理解起来方便一点。迈克老狼的博文都不错,大家可以参考下。

              image

这个图演示了opencl编程的基本流程。

       首先要创建平台对象Platform,然后创建GPU设备Device,然后创建上下文环境context,其次是创建command队列,接下来创建opencl内存对象,用来存放需要计算的输入和输出数据,创建kernel程序program对kernel代码进行编译,创建kernel对象,设置kernel参数并执行,将结果返回host并输出。

Context是指管理OpenCL对象和资源的上下文环境。为了管理OpenCL程序,下面的一些对象都要和Context关联起来:

—设备(Devices):执行Kernel程序对象。

—程序对象(Program objects): kernel程序源代码

Kernels:运行在OpenCL设备上的函数。

—内存对象(Memory objects): device处理的数据对象。

—命令队列(Command queues): 设备之间的交互机制。

 

     GPU的优势在于并行计算,我们先看看OpenCL中的线程结构:

大规模并行程序中,通常每个线程处理一个问题的一部分,比如向量加法,我们会把两个向量中对应的元素加起来,这样每个线程可以处理一个加法。下面我看一个16个元素的向量加法:两个输入缓冲A、B,一个输出缓冲C,在这种情况下,我们可以创建一维的线程结构去匹配这个问题。每个线程把自己的线程id作为索引,把相应元素加起来。原理如下图。

 

image

 OpenCL中的线程结构是可缩放的,Kernel的每个运行实例称作WorkItem(也就是线程),WorkItem组织在一起称作WorkGroup,OpenCL中,每个Workgroup之间都是相互独立的。

通过一个global id(在索引空间,它是唯一的)或者一个workgroup id和一个work group内的local id,就能标定一个workitem。如下图所示。

image

 

 

 

 

接下来用一个简单的程序演示一下:计算两个数组相加的和,放到另一个数组中去。为了简化代码方便理解,我们初始化数组buf1[]=buf2[]=0,1,2,3...并用cpu和gpu分别计算出结果进行验证。

代码如下:

#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <iostream>
#include <fstream>

using namespace std;
#define BUFSIZE 1024

#pragma comment (lib,"OpenCL.lib")

//该函数把kernel源文件读入到一个string中,用来读入kernel源文件,kernel源文件可以新建一个.cl文件单独存放。
int convertToString(const char *filename, std::string& s)
 {
 size_t size;
 char*  str;

 std::fstream f(filename, (std::fstream::in | std::fstream::binary));

 if(f.is_open())
  {
  size_t fileSize;
  f.seekg(0, std::fstream::end);
  size = fileSize = (size_t)f.tellg();
  f.seekg(0, std::fstream::beg);

  str = new char[size+1];
  if(!str)
   {
   f.close();
   return NULL;
   }

  f.read(str, fileSize);
  f.close();
  str[size] = '\0';

  s = str;
  delete[] str;
  return 0;
  }
 printf("Error: Failed to open file %s\n", filename);
 return 1;
 }

//等待事件完成 ,通过event来查询我们的操作是否完成,没完成的话,程序就一直block在这行代码处,这个函数和clWaitForEvents其实效果一样,但是我发现在做有关图像的程序时最好用clWaitForEvents,不然会内存冲突造成蓝屏。

int waitForEventAndRelease(cl_event *event)
 {
 cl_int status = CL_SUCCESS;
 cl_int eventStatus = CL_QUEUED;
 while(eventStatus != CL_COMPLETE)
  {
  status = clGetEventInfo(
   *event,
   CL_EVENT_COMMAND_EXECUTION_STATUS,
   sizeof(cl_int),
   &eventStatus,
   NULL);
  }

 status = clReleaseEvent(*event);

 return 0;
 }
int main(int argc, char* argv[])
 {
 //在host内存中创建三个缓冲区存放buf1,buf2,buf,buf1和buf2是输入数组,buf存放buf1和buf2的和。
 int *buf1 = 0;
 int *buf2 = 0;
 int *buf = 0;

 buf1 =(int *)malloc(BUFSIZE * sizeof(int));
 buf2 =(int *)malloc(BUFSIZE * sizeof(int));
 buf =(int *)malloc(BUFSIZE * sizeof(int));

 //用一些随机值初始化buf1和buf2的内容,这里我们用0,1,2,3...
 int i;
 //srand( (unsigned)time( NULL ) );
 for(i = 0; i < BUFSIZE; i++)
 {
  buf1[i] = i;
 }

 //srand( (unsigned)time( NULL ) +1000);
 for(i = 0; i < BUFSIZE; i++)
 { 
  buf2[i] = i;
 }

 //cpu计算buf1,buf2的和
 for(i = 0; i < BUFSIZE; i++)
 {
     buf[i] = buf1[i] + buf2[i];
 }


 cl_uint status;
 cl_platform_id platform;

 //创建平台对象
 //status = clGetPlatformIDs( 1, &platform, NULL );这行代码如果在只有一个平台的情况下可以使用,如果有多个opencl平台就会报错,就要使用如下的代码。
 cl_uint numPlatforms;
 std::string platformVendor;
 status = clGetPlatformIDs(0, NULL, &numPlatforms);
 if(status != CL_SUCCESS)
  {
  return 0;
  }
 if (0 < numPlatforms)
  {
  cl_platform_id* platforms = new cl_platform_id[numPlatforms];
  status = clGetPlatformIDs(numPlatforms, platforms, NULL);

  char platformName[100];
  for (unsigned i = 0; i < numPlatforms; ++i)
   {
   status = clGetPlatformInfo(platforms[i],
    CL_PLATFORM_VENDOR,
    sizeof(platformName),
    platformName,
    NULL);

   platform = platforms[i];
   platformVendor.assign(platformName);

   if (!strcmp(platformName, "Advanced Micro Devices, Inc."))
    {
    break;
    }
   }

  std::cout << "Platform found : " << platformName << "\n";
  delete[] platforms;
  }
 cl_device_id device;

 //创建GPU设备
 clGetDeviceIDs( platform, CL_DEVICE_TYPE_GPU,
  1,
  &device,
  NULL);
 //创建context
 cl_context context = clCreateContext( NULL,
  1,
  &device,
  NULL, NULL, NULL);
 //创建命令队列
 cl_command_queue queue = clCreateCommandQueue( context,
  device,
  CL_QUEUE_PROFILING_ENABLE, NULL );
 //创建三个OpenCL内存对象,并把buf1的内容通过隐式拷贝的方式拷贝到clbuf1,buf2的内容通过显示拷贝的方式拷贝到clbuf2,这里只是告诉大家隐式和显式的代码的区别。
 cl_mem clbuf1 = clCreateBuffer(context,
  CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR,
  BUFSIZE*sizeof(cl_uint),buf1,
  NULL );

 cl_mem clbuf2 = clCreateBuffer(context,
  CL_MEM_READ_ONLY ,
  BUFSIZE*sizeof(cl_uint),NULL,
  NULL );

 cl_event writeEvt;
 status = clEnqueueWriteBuffer(queue, clbuf2, 1,
  0, BUFSIZE*sizeof(cl_uint), buf2, 0, 0, &writeEvt);
 status = clFlush(queue);
 //等待数据传输完成再继续往下执行
 waitForEventAndRelease(&writeEvt);
 //clWaitForEvents(1, &writeEvt);

 cl_mem buffer = clCreateBuffer( context,
  CL_MEM_WRITE_ONLY,
  BUFSIZE * sizeof(cl_uint),
  NULL, NULL );

 //kernel文件为add.cl
 const char * filename  = "add.cl";
 std::string  sourceStr;
 status = convertToString(filename, sourceStr);
 const char * source    = sourceStr.c_str();
 size_t sourceSize[]    = { strlen(source) };

 //创建程序对象
 cl_program program = clCreateProgramWithSource(
  context,
  1,
  &source,
  sourceSize,
  NULL);
 //编译程序对象
 status = clBuildProgram( program, 1, &device, NULL, NULL, NULL );
 if(status != 0)
  {
  printf("clBuild failed:%d\n", status);
  char tbuf[0x10000];
  clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0x10000, tbuf, NULL);
  printf("\n%s\n", tbuf);
  return -1;
  }

 //创建Kernel对象
 cl_kernel kernel = clCreateKernel( program, "vecadd", NULL );
 //设置Kernel参数
 cl_int clnum = BUFSIZE;
 clSetKernelArg(kernel, 0, sizeof(cl_mem), (void*) &clbuf1);
 clSetKernelArg(kernel, 1, sizeof(cl_mem), (void*) &clbuf2);
 clSetKernelArg(kernel, 2, sizeof(cl_mem), (void*) &buffer);

 //执行kernel,首先要设置线程索引空间的维数以及workgroup大小等。Range用1维,work itmes size为BUFSIZE,没有设置group size时,系统会使用默认的work group size,通常可能是256
 cl_event ev;
 size_t global_work_size = BUFSIZE;
 clEnqueueNDRangeKernel( queue,
  kernel,
  1,
  NULL,
  &global_work_size,
  NULL, 0, NULL, &ev);
 status = clFlush( queue );
 waitForEventAndRelease(&ev);
    //clWaitForEvents(1, &ev);

 //当Kernel函数执行完毕后,我们要把数据从device memory中拷贝到host memory中去。
 cl_uint *ptr;
 cl_event mapevt;
 ptr = (cl_uint *) clEnqueueMapBuffer( queue,
  buffer,
  CL_TRUE,
  CL_MAP_READ,
  0,
  BUFSIZE * sizeof(cl_uint),
  0, NULL, &mapevt, NULL );
 status = clFlush( queue );
 waitForEventAndRelease(&mapevt);
 //clWaitForEvents(1, &mapevt);

 //结果验证,和cpu计算的结果比较
 if(!memcmp(buf, ptr, BUFSIZE))
  printf("Verify passed\n");
 else printf("verify failed");

 if(buf)
  free(buf);
 if(buf1)
  free(buf1);
 if(buf2)
  free(buf2);

 printf("Result=\n");
 for(int m=0;m<BUFSIZE;m++)
    printf("%d ",ptr[m]);
   
 cin.get();
 

 //删除OpenCL资源对象,大多数的OpenCL资源都是指针,不使用的时候需要释放掉。
 clReleaseMemObject(clbuf1);
 clReleaseMemObject(clbuf2);
 clReleaseMemObject(buffer);
 clReleaseProgram(program);
 clReleaseCommandQueue(queue);
 clReleaseContext(context);
 return 0;
 }

 

 

 

kernel函数就是gpu的计算部分了,这个示例的代码很简单,如下:

 __kernel void vecadd(__global const uint* A, __global const uint* B, __global uint* C)
{   
int id = get_global_id(0);  
 C[id] = A[id] + B[id];
 }

 

 

每个Kernel函数都必须以__kernel开始,而且必须返回void。每个输入参数都必须声明使用的内存类型。通过一些API,比如get_global_id之类的得到线程id。

内存对象地址空间标识符有以下几种:

__global

__local

__private

__constant

 

这四个分别对应了CL架构中的存储区域(设备全局、work group、compute unit 、设备constant)。目前global一定是constant的,也就是声明global时必须赋值 (global就等于 global constant),Kernel函数参数如果是内存对象,那么一定是__global,__local或者constant。

如图所示:get_global_id是获取总的线程数,比如设置kernel的work group size是256,数组大小为512,那就是有两个group,group_id[2]={0,1},每个group里面的线程的唯一id为local_id,local_size=group size。

 

 

 

 

转载于:https://www.cnblogs.com/newbiecode/p/3151943.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值