本系列代码托管于:https://github.com/chintsan-code/cuda_by_example
本篇使用的项目为:julia_cpu、julia_gpu
上一篇笔记实现了GPU上的矢量相加,本篇实现绘制Julia集。对于Julia集,只需要理解成满足某个公式的得所有点构成的集合即可,不用深入理解,这个公式是:
$$Z_{n+1} = Z_n^2 + C$$
可以看到,这个公式的迭代过程为:首先计算当前值得平方,然后再加上一个常数,就得到等式的下一个值。
一. 基于CPU的Julia集绘制
// julia_cpu
#include <stdio.h>
#include "../../common/cpu_bitmap.h"
#define DIM 1000
struct cuComplex
{
float r; // 复数的实数部分
float i; // 复数的虚数部分
cuComplex(float a, float b) :r(a), i(b) { }
float magnitude2(void)
{
return r * r + i * i; // 复数的模的平方
}
cuComplex operator * (const cuComplex& a)
{
return cuComplex(r * a.r - i * a.i, i * a.r + r * a.i);
}
cuComplex operator + (const cuComplex& a)
{
return cuComplex(r + a.r, i + a.i);
}
};
int julia(int x, int y)
{
const float scale = 1.5;
// DIM / 2 - x、DIM / 2 - y将原点定位到图像中心
// 除以(DIM / 2)是为了确保图像的范围为[-1.0, 1.0]
// scale是用来缩放图像的,可以自行修改
float jx = scale * (float)(DIM / 2 - x) / (DIM / 2);
float jy = scale * (float)(DIM / 2 - y) / (DIM / 2);
cuComplex c(-0.8, 0.156); // 复数C = -0.8 + 0.156i
cuComplex z(jx, jy);
int i = 0;
for (i = 0; i < 200; i++)
{
z = z * z + c; //Zn+1 = Zn^2 + C
if (z.magnitude2() > 1000)
{
// 迭代200次,每次迭代完都判断结果是否超过阈值(这里是1000),如果超过就不属于julia集
return 0;
}
}
return 1; // 属于Julia集
}
void kernel(unsigned char* ptr)
{
for (int y = 0; y < DIM; y++)
{
for (int x = 0; x < DIM; x++)
{
int offset = x + y * DIM; // 像素在内存中的线性偏移,因为图像在内存中实际是一维存储的
int juliaValue = julia(x, y); // 判断点(x, y)是否属于Julia集合,属于返回1,不属于返回0
// juliaValue为0时为黑色(0,0,0),为1时为红色(255,0,0)
ptr[offset * 4 + 0] = 255 * juliaValue; // red通道
ptr[offset * 4 + 1] = 0; // green通道
ptr[offset * 4 + 2] = 0; // blue通道
ptr[offset * 4 + 3] = 255; // alpha通道
}
}
}
int main()
{
CPUBitmap bitmap(DIM, DIM);
unsigned char* ptr = bitmap.get_ptr();
kernel(ptr); // 将指向图像的指针传递给核函数
bitmap.display_and_exit();
getchar();
return 0;
}
首先看主函数main
:
int main()
{
CPUBitmap bitmap(DIM, DIM);
unsigned char* ptr = bitmap.get_ptr();
kernel(ptr); // 将指向图像的指针传递给核函数
bitmap.display_and_exit();
getchar();
return 0;
}
通过工具库创建一张位图,大小是DIM * DIM。然后获取该图像的指针,将其传递给核函数,最后将图像显示处理。
然后是核函数kernel
:
void kernel(unsigned char* ptr)
{
for (int y = 0; y < DIM; y++)
{
for (int x = 0; x < DIM; x++)
{
int offset = x + y * DIM;
int juliaValue = julia(x, y); // 判断点(x, y)是否属于Julia集合,属于返回1,不属于返回0
// juliaValue为0时为黑色(0,0,0),为1时为红色(255,0,0)
ptr[offset * 4 + 0] = 255 * juliaValue; // red通道
ptr[offset * 4 + 1] = 0; // green通道
ptr[offset * 4 + 2] = 0; // blue通道
ptr[offset * 4 + 3] = 255; // alpha通道
}
}
}
核函数对图像中的所有点进行后台绘制,一共有DIM * DIM个点,其中会调用julia
函数对当前点进行判断,判断其是否属于Julia集,如果属于,将这个点填充为红色;如果不属于,填充为黑色。
julia
函数和cuComplex
结构体都是为了Julia集服务了,书中有介绍,这里不写了,仅对关键部分做了注释。
二. 基于GPU的Julia集绘制
// julia_gpu
#include "cuda_runtime.h"
#include "device_launch_parameters.h"
#include <stdio.h>
#include "../../common/book.h"
#include "../../common/cpu_bitmap.h"
#define DIM 1000
struct cuComplex
{
float r;
float i;
// cuComplex( float a, float b ) : r(a), i(b) {}
__device__ cuComplex(float a, float b) : r(a), i(b) { } // __device__表示代码将在设备而不是主机上运行。
// 由于声明为__device__函数,因此只能从其他__device__
// 或__global__函数中调用他们
__device__ float magnitude2(void)
{
return r * r + i * i;
}
__device__ cuComplex operator * (const cuComplex& a)
{
return cuComplex(r * a.r - i * a.i, i * a.r + r * a.i);
}
__device__ cuComplex operator + (const cuComplex& a)
{
return cuComplex(r + a.r, i + a.i);
}
};
__device__ int julia(int x, int y)
{
const float scale = 1.5;
float jx = scale * (float)(DIM / 2 - x) / (DIM / 2);
float jy = scale * (float)(DIM / 2 - y) / (DIM / 2);
cuComplex c(-0.8, 0.156);
cuComplex a(jx, jy);
int i = 0;
for (i = 0; i < 200; i++)
{
a = a * a + c;
if (a.magnitude2() > 1000)
return 0;
}
return 1;
}
__global__ void kernel(unsigned char* ptr) {
// 将threadIdx/blockIdx映射到像素位置
int x = blockIdx.x;
int y = blockIdx.y;
int offset = x + y * gridDim.x; // 对所有的线程块而言,gridDim是常数,用来保存线程格每一维的大小,
// gridDim.x是线程格的宽度
int juliaValue = julia(x, y);
ptr[offset * 4 + 0] = 255 * juliaValue;
ptr[offset * 4 + 1] = 0;
ptr[offset * 4 + 2] = 0;
ptr[offset * 4 + 3] = 255;
}
int main() {
CPUBitmap bitmap(DIM, DIM);
unsigned char* dev_ptr;
HANDLE_ERROR(cudaMalloc((void**)&dev_ptr, bitmap.image_size()));
dim3 grid(DIM, DIM); // 二维线程格(Grid)。dim3表示一个三维数组,在这里第三维其实就是1,同dim3 grid(DIM, DIM, 1)
kernel<<<grid, 1>>>(dev_ptr);
HANDLE_ERROR(cudaMemcpy(bitmap.get_ptr(), dev_ptr, bitmap.image_size(), cudaMemcpyDeviceToHost));
bitmap.display_and_exit();
HANDLE_ERROR(cudaFree(dev_ptr));
return 0;
}
由于在这个绘制情景下,由于图像中的每个点的计算都是独立的,计算结果与其他点无关,因此可以指定多个线程并行执行kernel
函数。而且在这个图像处理的问题中,Block的二维索引会带来非常大的便利。
首先声明了一个三维的线程格(Grid):
dim3 grid(DIM, DIM);
类型dim3并不是标准C中定义的类型,它被定义在CUDA相关的头文件中,它表示一个三维数组,可以用于指定启动的Block的数量,在这里第三维其实就是1。因此下面的代码表示告诉CUDA Runtime,我们需要创建一个二维的Grid,其中包含DIM * DIM个Block。
dim3 grid(DIM, DIM);
kernel<<<grid, 1>>>(dev_ptr);
回到kernel
函数:
__global__ void kernel(unsigned char* ptr) {
int x = blockIdx.x;
int y = blockIdx.y;
int offset = x + y * gridDim.x; // 对所有的线程块而言,gridDim是常数,用来保存线程格每一维的大小,
// gridDim.x是线程格的宽度
int juliaValue = julia(x, y);
ptr[offset * 4 + 0] = 255 * juliaValue;
ptr[offset * 4 + 1] = 0;
ptr[offset * 4 + 2] = 0;
ptr[offset * 4 + 3] = 255;
}
与CPU版本不同的是,不再需要双重for循环来逐个生成图像的每一个像素的索引,CUDA Runtime将在变量blockIdx
中包含这些像素索引,由于我们设置了合适的Grid和Block,因此每一个像素都可以获得一个Block进行渲染绘制。
接下来需要得到ptr
中的线性偏移(与基于CPU的一致,因为图像在内存中实际是一维存储的)。这个偏移值通过内置变量gridDim
来得到,实际上,但我们告诉CUDA Runtime需要创建一个包含DIM*DIM个Block的二维Grid时,这个 gridDim
变量就已经被确定了,相当于常数,大小为(DIM, DIM)
julia
函数和cuComplex
结构体和CPU版本的基本一致,二者的差异主要在多了修饰符__device__
,这表示该段代码将在GPU而不是主机上运行。由于这些函数已声明为__device__
函数,因此只能从其他的__device__
函数或__global__
函数中调用它们。
参考:
- 《GPU高性能编程 CUDA实战》
评论 (0)