目录
1.背景提出:
内存带宽对性能瓶颈的影响?
2.解决方案:
3.案例分析:光线追踪(Ray Tracing)实例
简介:
构造:
功能:
代码复现--非常量内存GPU版本:
代码复现--常量内存GPU版本:
修改一:__constant__
修改二:cudaMemcpyToSymbol()
4.Constant Memory带来的性能分析提升
从Constant Memory读取相同的数据可以节约内存带宽的源因:
线程束(Warp):
节约内存带宽的机制:
在光线追踪器程序中的分析:
双刃剑:
性能瓶颈通常并不在于芯片的数学计算吞吐量,而是在于芯片的内存带宽。由于GPU上包含非常多的ALU,因此有时输入数据的速率甚至无法维持如此高的计算速率。因此有必要研究一些手段来减少计算问题时的内存通信量。
除了Global Memory、Shared Memory外,CUDA C程序还支持另一种类型的内存,Constant Memory。Contant Memory用于保存在Kernel执行期间不会发生变化的数据。NVIDIA硬件提供64KB的Constan Memory。在某些情况,用Constant Memory替换Global Memory能有效的减少内存带宽。
从三维场景中生成一张二维图像的一种方式。原理:在场景中选择一个位置放上一台假想的相机。这台数字相机包含一个光传感器来生成图像,因此需要判断哪些光将接触到这个传感器。图像中的每个像素与命中传感器的光线有着相同的颜色和强度。
由于传感器中的命中的光线可能来自场景中的任意位置。因此事实也证明了采用逆向计算或许是更容易实现的。也就是说,不是找出哪些光线将命中某个像素,而是想象从该像素发出一道射线进入场景中,按照这种思路,每个像素的行为都像一只“观察”场景的眼镜,即每个像素投射光纤进入到场景的过程。
我们追踪从像素中投射出的光纤穿过场景,直到光纤命中某个物体,然后计算这个像素的颜色。我们说像素都将“看到”这个物体,并根据它所看到物体的颜色来设置它的颜色。光纤追踪中的大部分计算都是光线与场景中物体的相交运算。
只支持一组包含球状物体的场景,并且相机被固定在Z轴,面向圆原点。此外,我们将不支持场景中的任何照明,从而避免二次光线带来的复杂性。也不支持计算照明的效果,而只是为每个球面分配一个颜色值,然后如果他们是可见的,则通过某个预先计算的值对其着色。
从每个像素发射一道光线,并且跟踪这些光线会命中那些球面。此外,它还将跟踪每道命中光线的深度。当一道光线穿过多个球面时,只有最接近相机的球面才会被看到,我们的“光线跟踪器”会把相机看不到的球面隐藏起来。
其实我也不懂图像,作者把有关图像的内容封装成了包,在参考代码直接调用就好了。
#include "cuda.h"
#include "../common/book.h"
#include "../common/cpu_bitmap.h"
#define DIM 1024
#define rnd( x ) (x * rand() / RAND_MAX)
#define INF 2e10f
//通过一个数据结构对球面建模
struct Sphere {
float r, b, g; //颜色值
float radius; //半径
float x, y, z; //球面的中心坐标
/*
该方法对来自(ox,oy)处像素的光线,这个方法将计算光线是否与这个球面相交。如果光线与球面相交,
那么这个方法将计算从相机到光线命中球面处的距离。
需要这个函数的原因:当光线命中多个球面时,只有最接近相机的球面才会被看见。
*/
__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;
}
};
#define SPHERES 20
/*
执行光线追踪计算并且从输入的一组球面中为每个像素计算颜色数据。
最后,我们将把输出图像从GPU中复制回来,并显示它
*/
/*
每个线程都会为输出影响中的一个像素计算颜色值,计算每个线程对应的x坐标和y坐标,
并且根据这两个坐标来计算输出缓冲区中的偏移,此外,我们还将把图像坐标(x,y)偏移DIM/2
这样Z轴将穿过图像的中心
*/
__global__ void kernel(Sphere *s, unsigned char *ptr) {
//将threadIdx/BlockIdx映射到像素位置
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.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 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;
}
// globals needed by the update routine
struct DataBlock {
unsigned char *dev_bitmap;
Sphere *s;
};
int main(void) {
DataBlock data;
//记录(capture) 起始时间
cudaEvent_t start, stop;
HANDLE_ERROR(cudaEventCreate(&start));
HANDLE_ERROR(cudaEventCreate(&stop));
HANDLE_ERROR(cudaEventRecord(start, 0));
CPUBitmap bitmap(DIM, DIM, &data);
unsigned char *dev_bitmap;
Sphere *s;
//在GPU上分配内存以计算输出位图(output bitmap)
HANDLE_ERROR(cudaMalloc((void**)&dev_bitmap,bitmap.image_size()));
//为Sphere数据集分配内存
HANDLE_ERROR(cudaMalloc((void**)&s,sizeof(Sphere) * SPHERES));
/*为输入数据分配内存,这些数据是一个构成场景的Sphere数组。Sphere数组在CPU上生成并在GPU上使用,
因此我们调用cudaMalloc()和malloc()在GPU和CPU上分配内存。此外,我们还需要一张位图图像,
当在GPU上计算光线跟踪球面时,使用计算得到的像素值来填充这种图像,*/
//分配临时内存,对其初始化,并复制到GPU上的内存,然后释放临时内存
Sphere *temp_s = (Sphere*)malloc(sizeof(Sphere) * SPHERES);
//程序将生成一个包含20个球面的随机数组,通过#define宏指定的
for (int i = 0; i> >(s, dev_bitmap);
//将位图从GPU复制回到CPU以显示
HANDLE_ERROR(cudaMemcpy(bitmap.get_ptr(), dev_bitmap, bitmap.image_size(),cudaMemcpyDeviceToHost));
//停止计时,并显示事件结果
HANDLE_ERROR(cudaEventRecord(stop, 0));
HANDLE_ERROR(cudaEventSynchronize(stop));
float elapsedTime;
HANDLE_ERROR(cudaEventElapsedTime(&elapsedTime,start, stop));
printf("Time to generate: %3.1f ms\n", elapsedTime);
HANDLE_ERROR(cudaEventDestroy(start));
HANDLE_ERROR(cudaEventDestroy(stop));
HANDLE_ERROR(cudaFree(dev_bitmap));
HANDLE_ERROR(cudaFree(s));
// display
bitmap.display_and_exit();
}
wlsh@wlsh-ThinkStation:~/Desktop/GPU高性能编程CUDA实战—示例代码/chapter06$ nvcc -o ra_noconst ray_noconst.cu -lglut -lGL -lGLU
../common/cpu_bitmap.h(49): warning: conversion from a string literal to "char *" is deprecated
../common/cpu_bitmap.h(49): warning: conversion from a string literal to "char *" is deprecated
wlsh@wlsh-ThinkStation:~/Desktop/GPU高性能编程CUDA实战—示例代码/chapter06$ ./ra_noconst
Time to generate: 3.9 ms
Constant Memory是不可以修改的,因此显然无法用来保存输出图像的数据。因为只有一个输入数组,即球面数组,因此应该把这个数据保存到常量内存中。
先前版本,声明指针,然后通过cudaMalloc()来为指针分配GPU内存。当我们将其修改为常量内存时,同样要将这个声明修改为在常量内存中静态地分配空间,在编译时为数组提交一个固定大小。
/*
Sphere *s;
HANDLE_ERROR( cudaMalloc( (void**)&s,sizeof(Sphere) * SPHERES ) );
*/
__constant__ Sphere s[SPHERES];
当需要从Host内存复制到GPU上的Constant Memory时,需要使用特殊版本的cudaMemcpy()。cudaMemcpyToSymbol()会复制到constant Memory,而cudaMemcpy()会复制到Global Memory.
HANDLE_ERROR( cudaMemcpyToSymbol( s, temp_s, sizeof(Sphere) * SPHERES) );
#include "cuda.h"
#include "../common/book.h"
#include "../common/cpu_bitmap.h"
#define DIM 1024
#define rnd( x ) (x * rand() / RAND_MAX)
#define INF 2e10f
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;
}
};
#define SPHERES 20
/*
Sphere *s;
HANDLE_ERROR( cudaMalloc( (void**)&s,sizeof(Sphere) * SPHERES ) );
*/
__constant__ Sphere s[SPHERES];
__global__ void kernel( unsigned char *ptr ) {
// map from threadIdx/BlockIdx to pixel position
int x = threadIdx.x + blockIdx.x * blockDim.x;
int y = threadIdx.y + blockIdx.y * blockDim.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 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;
}
// globals needed by the update routine
struct DataBlock {
unsigned char *dev_bitmap;
};
int main( void ) {
DataBlock data;
// capture the start time
cudaEvent_t start, stop;
HANDLE_ERROR( cudaEventCreate( &start ) );
HANDLE_ERROR( cudaEventCreate( &stop ) );
HANDLE_ERROR( cudaEventRecord( start, 0 ) );
CPUBitmap bitmap( DIM, DIM, &data );
unsigned char *dev_bitmap;
// allocate memory on the GPU for the output bitmap
HANDLE_ERROR( cudaMalloc( (void**)&dev_bitmap,
bitmap.image_size() ) );
// allocate temp memory, initialize it, copy to constant
// memory on the GPU, then free our temp memory
Sphere *temp_s = (Sphere*)malloc( sizeof(Sphere) * SPHERES );
for (int i=0; i>>( dev_bitmap );
// copy our bitmap back from the GPU for display
HANDLE_ERROR( cudaMemcpy( bitmap.get_ptr(), dev_bitmap,
bitmap.image_size(),
cudaMemcpyDeviceToHost ) );
// get stop time, and display the timing results
HANDLE_ERROR( cudaEventRecord( stop, 0 ) );
HANDLE_ERROR( cudaEventSynchronize( stop ) );
float elapsedTime;
HANDLE_ERROR( cudaEventElapsedTime( &elapsedTime,
start, stop ) );
printf( "Time to generate: %3.1f ms\n", elapsedTime );
HANDLE_ERROR( cudaEventDestroy( start ) );
HANDLE_ERROR( cudaEventDestroy( stop ) );
HANDLE_ERROR( cudaFree( dev_bitmap ) );
// display
bitmap.display_and_exit();
}
wlsh@wlsh-ThinkStation:~/Desktop/GPU高性能编程CUDA实战—示例代码/chapter06$ nvcc -o ray ray.cu -lglut -lGL -lGLU
../common/cpu_bitmap.h(49): warning: conversion from a string literal to "char *" is deprecated
../common/cpu_bitmap.h(49): warning: conversion from a string literal to "char *" is deprecated
wlsh@wlsh-ThinkStation:~/Desktop/GPU高性能编程CUDA实战—示例代码/chapter06$ ./ray
Time to generate: 1.0 ms
__constant__ 关键字把变量的访问限制为只读。
(1)对Constant Memory的单次读操作可以广播到其他的“邻近(Nearby)”线程,这将节约15次读取操作。
(2)Constant Memory的数据将缓存起来,因此对相同地址的连续读操作不会产生额外的内存通信量。
在CUDA架构中,Warp是一个包含32个Thread的集合,这个Thread集合被“编织在一起”并且以“步调一致(Lockstep)”的形式执行。在程序的每一行,Warp中的每个Thread都将在不同的数据上执行相同的指令。
当处理Constant Memory时,NVIDIA硬件把单次内存读取操作广播到每个半线程束(Half-Warp),即16个Thread。如果在Half-Warp中的每个Thread都从Constant Memory的相同地址上读取数据,那么GPU只会产生一次读取请求并在随后将数据广播到每个Thread。如果从Constant中读取大量的数据,那么这种方式产生的内存流量只是使用Global Memory的1/16(6%)。
在读取Constant Memory时,所节约的不仅限于减少了94%的带宽,由于这块内存的内容是不会发生变化的,因此硬件将主动把这个常量数据缓存在GPU上。在第一次从常量内存的某个地址上读取后,当其他Half-Warp请求同一个地址时,那么将命中缓存,这同样也减少了额外的内存流量。
每个Thread都要读取球面的相应数据从而计算它与光线的相交情况。在把应用程序修改为将球面数据保存在Constant Memory后,硬件只需要请求这个数据一次。在缓存数据后,其他每个Thread将不会产生内存流量,原因有两个:
(1)Thread将在Half-Warp的广播中收到这个数据
(2)从Constatn Memory缓存中收到数据。
Half-Warp功能实际上是一把双刃剑。虽然当所有16个Thread都读取相同地址时,这个功能可以极大的提升性能。但当所有16个Thread分别读取不同的地址时,它实际上会降低性能。
故,只有当16个Thread每次都只需要相同的读取请求时,才值得讲这个读取操作广播到16个Thread。然而,如果Half-Warp中的所有16个Thread需要访问Constant Memory中不同的数据,那么这个16次不同的读取操作会被串行化,从而需要16倍的时间来发出请求。但如果从Global Memory中读取,那么这些请求会同时发出。在这种情况下,从Constant Memory读取就慢于Global Memory中读取。