CUDA By Example(三)

        GPU性能的瓶颈往往不在于芯片的数学计算吞吐量,而在于芯片的内存宽带。GPU有非常多的数字逻辑单元(ALU),因此有时输入数据的速率无法维持如此高的计算速率。
         CUDA C除了可以使用全局内存和共享内存,还支持常量内存。常量内存用于保持核函数执行期间不会发生变化的数据。NVIDA提供了64KB的常量内存。在某些情况下,用常量内存替代全局内存能有效减少内存宽带。

 

         下面的例子演示光线跟踪。

         光线跟踪(Ray Tracing)是从三维对象场景中生成二维图像的一种方式。(OpenGL、DirectX中有一种相同目的的技术:光栅化,Rasterization)。光线跟踪的原理是:在场景中选择一个位置放一台假象的相机,这台相机用光传感器来生成图像,图像的每个像素与命中传感器的光线有着相同的颜色和强度。命中的光线可能来自场景中的任意位置,因此采用逆向计算跟容易一些。从像素中投射出的光线穿过场景,直到光像命中某个物体,然后计算这个像素的颜色,这里称像素将“看到”这个物体。光线跟踪中的大部分计算都是光线与场景中物体的相交运算。
         更复杂的光线跟踪模型中,场景中会有反射光线和折射光线出现,这将生成二次射线、三次射线等。

#include "cuda_runtime.h"  
#include "device_launch_parameters.h"  
#include "device_functions.h" 
#include <stdio.h>
#include <math.h>
#include "bitmap.h"

#define INF 2e10f
#define SPHERES 20
#define DIM 1024
#define rnd( x )  (x*rand()/RAND_MAX)

struct Sphere{
	float r,b,g;
	float radius;
	float x,y,z;
	__device__ float hit(float ox,float oy,float *n){  
	    	float dx=ox-x;
	      	float dy=oy-y;
	    	if(dx*dx+dy*dy<radius*radius){
	    	   float dz=sqrtf(radius*radius-dx*dx-dy*dy);
	    	   *n=dz/sqrtf(radius*radius);
	    	   return dz+z;
    	    }
    	    return -INF;
    	 }
    };

__global__ void kernel(unsigned char*ptr ,Sphere *s);

int main(){
	Sphere *s;
	cudaEvent_t start,stop;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);
	cudaEventRecord(start,0);

	Bitmap bitmap(DIM ,DIM);
	unsigned char *dev_bitmap;
	cudaMalloc((void**)&dev_bitmap,bitmap.image_size());  //给dev_bitmap分配内存
	cudaMalloc((void **)&s,sizeof(Sphere)*SPHERES);  //给s分配内存

	Sphere *temp_s=(Sphere*)malloc(sizeof(Sphere)*SPHERES);//在CPU上先给s一个拷贝
	for(int i=0;i<SPHERES;i++){  //生成20个随机数组给temp_s赋值
		temp_s[i].r=rnd(1.0f);
		temp_s[i].g=rnd(1.0f);
		temp_s[i].b=rnd(1.0f);
		temp_s[i].x=rnd(1000.0f)-500;
		temp_s[i].y=rnd(1000.0f)-500;
		temp_s[i].z=rnd(1000.0f)-500;
		temp_s[i].radius=rnd(100.0f)+20;
	}

	cudaMemcpy(s,temp_s,sizeof(Sphere)*SPHERES,cudaMemcpyHostToDevice);
	free(temp_s);

	//数据都分配到GPU上了,接下来启动核函数
	dim3 grids(DIM/16,DIM/16);
	dim3 threads(16,16);
	kernel<<<grids,threads>>>(dev_bitmap,s);

	cudaMemcpy(bitmap.get_ptr(),dev_bitmap,bitmap.image_size(),cudaMemcpyDeviceToHost);

	cudaEventRecord(stop,0);
	cudaEventSynchronize(stop);
	float elapsedTime;
	cudaEventElapsedTime(&elapsedTime,start,stop);
	printf("time to generate:%3.1f 毫秒",elapsedTime);
	cudaEventDestroy(start);
	cudaEventDestroy(stop);

	bitmap.display_and_exit();
	cudaFree(dev_bitmap);
	cudaFree(s);
}


__global__ void kernel(unsigned char*ptr,Sphere *s){
	int x=threadIdx.x+blockDim.x*blockIdx.x;
	int y=threadIdx.y+blockDim.y*blockIdx.y;
	int offset=x+y*blockDim.x*gridDim.x;
	float ox=(x-DIM/2);
	float oy=(y-DIM/2);

	float r=0,g=0,b=0;
	float maxz=-INF;
	for(int i=0;i<SPHERES;i++){
		float n;
		float t=s[i].hit(ox,oy,&n);
		if(t>maxz){
			float fscale=n;
			r=s[i].r*fscale;
			g=s[i].g*fscale;
			b=s[i].b*fscale;
			maxz=t;
		}
	}

	ptr[offset*4+0]=(int)(r*255);
	ptr[offset*4+1]=(int)(g*255);
	ptr[offset*4+2]=(int)(b*255);
	ptr[offset*4+3]=255;
}

 

        程序思路大概如下:数据结构Sphere用来保存球的信息(颜色,位置,半径),Sphere中的hit用来计算(ox,oy)的像素的光线是否与这个球面相交。如果相交,那么将这个方法计算从相机到光线命中球面处的距离。然后随机产生一些Sphere,拷贝到GPU上。kernel是一个简单的光线跟踪模型,hit来判断像素点是否看到Sphere,如果比相机更近,那么作为新的最接近球面。

        运行结果:

 
CUDA By Example(三)_第1张图片
 

 

接下来是使用常量的版本。这里讲球Sphere数组设置为常量,常量需要使用需要注意的问题在程序中有说明。

#include "cuda_runtime.h"  
#include "device_launch_parameters.h"  
#include "device_functions.h" 
#include <stdio.h>
#include <math.h>
#include "bitmap.h"

#define INF 2e10f
#define SPHERES 200
#define DIM 1024
#define rnd( x )  (x*rand()/RAND_MAX)

struct Sphere{
	float r,b,g;
	float radius;
	float x,y,z;
	__device__ float hit(float ox,float oy,float *n){
	    	float dx=ox-x;
	      	float dy=oy-y;
	    	if(dx*dx+dy*dy<radius*radius){
	    	   float dz=sqrtf(radius*radius-dx*dx-dy*dy);
	    	   *n=dz/sqrtf(radius*radius);
	    	   return dz+z;
    	    }
    	    return -INF;
    	 }
    };

__global__ void kernel(unsigned char*ptr); //常量这里不需要将指针传递到函数里面去

__constant__ Sphere s[SPHERES];  //常量定义在外面

int main(){
	//Sphere *s;
	cudaEvent_t start,stop;
	cudaEventCreate(&start);
	cudaEventCreate(&stop);
	cudaEventRecord(start,0);

	Bitmap bitmap(DIM ,DIM);
	unsigned char *dev_bitmap;
	cudaMalloc((void**)&dev_bitmap,bitmap.image_size());
	//常量不需要分配内存,变量需要分配内存

	Sphere *temp_s=(Sphere*)malloc(sizeof(Sphere)*SPHERES);
	for(int i=0;i<SPHERES;i++){
		temp_s[i].r=rnd(1.0f);
		temp_s[i].g=rnd(1.0f);
		temp_s[i].b=rnd(1.0f);
		temp_s[i].x=rnd(1000.0f)-500;
		temp_s[i].y=rnd(1000.0f)-500;
		temp_s[i].z=rnd(1000.0f)-500;
		temp_s[i].radius=rnd(100.0f)+20;
	}

	cudaMemcpyToSymbol(s,temp_s,sizeof(Sphere)*SPHERES);  //常量拷贝过程用的函数不一样
	free(temp_s);

	dim3 grids(DIM/16,DIM/16);
	dim3 threads(16,16);
	kernel<<<grids,threads>>>(dev_bitmap);

	cudaMemcpy(bitmap.get_ptr(),dev_bitmap,bitmap.image_size(),cudaMemcpyDeviceToHost);

	cudaEventRecord(stop,0);
	cudaEventSynchronize(stop);
	float elapsedTime;
	cudaEventElapsedTime(&elapsedTime,start,stop);
	printf("time to generate:%3.1f 毫秒",elapsedTime);
	cudaEventDestroy(start);
	cudaEventDestroy(stop);

	bitmap.display_and_exit();
	cudaFree(dev_bitmap);
	//常量不需要free
}

__global__ void kernel(unsigned char*ptr){
	int x=threadIdx.x+blockDim.x*blockIdx.x;
	int y=threadIdx.y+blockDim.y*blockIdx.y;
	int offset=x+y*blockDim.x*gridDim.x;
	float ox=(x-DIM/2);
	float oy=(y-DIM/2);

	float r=0,g=0,b=0;
	float maxz=-INF;
	for(int i=0;i<SPHERES;i++){
		float n;
		float t=s[i].hit(ox,oy,&n);
		if(t>maxz){
			float fscale=n;
			r=s[i].r*fscale;
			g=s[i].g*fscale;
			b=s[i].b*fscale;
		}
	}

	ptr[offset*4+0]=(int)(r*255);
	ptr[offset*4+1]=(int)(g*255);
	ptr[offset*4+2]=(int)(b*255);
	ptr[offset*4+3]=255;
}

 

         __constant__把变量的访问限制为只读。与全局内存中读取数据相比,从常量内存中读取相同的数据可以节约内存带宽,因为:对常量内存的单次读取可以广播到其他的“近邻”线程;常量内存的数据将缓存起来,因此对相同地址的连续读操作将不会产生额外的内存通信量。
        这里所谓的“近邻”涉及到“线程束(wrap)”的概念。线程束可以看成是一组线程通过交织而形成的一个整体。CUDA中,线程束是指一个包含32个线程的集合,这个线程集合交织到一起并以步调一致的形式执行。在程序中的每一行,线程束中的每一个线程都将在不同的数据上执行相同的命令。
         当处理常量时,硬件将单次内存读取广播到每半个线程束(包含16个线程),这样,需要的内存流量大概是原始方式的1/16。硬件主动将这个常量数据存在GPU上,在第一次读取常量后,当其他半线程束请求同一数据的地址时,就会命中缓存。

        如果16个线程读取相同地址时候能够极大提升性能,如果16个线程分别读取不同地址可能会降低性能。

 

这里的cudaEvent用来统计执行时间。cudaEventRecord()视为一条记录当前时间的语句。当cudaEventSynchronize()返回时,stop之前的工作就完成了。这时就可以计算时间戳。

CUDA中的event是直接在GPU上实现的,他们不适合计算host与device的混合计时。

 

你可能感兴趣的:(example)