14.2_简单实现
14.2 简单实现
代码清单14-1给出了上一节描述的个体之间相互作用的函数实现,使用了__host__和__device__关键词双重修饰,CUDA编译器就会知道它既可以被CPU也可以被GPU调用。这个函数带有模板,因此它可以被float和double同时使用(在这本书中,只有float型是完全实现了的)。它返回三维作用力矢量,用三元组(ax,ay,az)[1]表示。
代码清单14-1 bodyBodyInteraction函数
template <typename T>
__host__device__void bodyBodyInteraction(
T& ax, T& ay, T& az,
T x0, T y0, T z0,
T x1, T y1, T z1, T mass1,
T softeningSquared)
{
T dx = x1 - x0;
T dy = y1 - y0;
T dz = z1 - z0;
T distSqr = dx * dx + dy * dy + dz * dz;
distSqr += softeningSquared;
T invDist = (T)1.0 / (T)sqrt(distSqr);
T invDistCube = invDist * invDist * invDist;
T s = mass1 * invDistCube;
ax = dx * s;
ay = dy * s;
az = dz * s;
}代码清单14-2给出了计算每个个体受到所有作用力的函数实现。对于每个个体,它把自己的位置加载到(myX, myY, myZ),然后调用函数bodyBodyInteraction来计算和其他每个个体之间的作用
力。函数名中的AOS,表示输入的数据来自一个结构体数组(array of structures,AOS)。包含四个float型数的结构体(x,y,z,mass)指定了个体的位置和质量。使用float4类型的数据是为了方便GPU的实现,这样可以得到硬件在加载和存储上的天然支持。而我们优化的CPU实现,详见第14.9小节,使用包含x、y、z和mass四个float型数组组成的结构数组(structure of arrays,SOA),这种形式能够有利于SIMD(单指令多数据)指令集的处理。结构数组不是很适合GPU实现,因为其中所需的四个基指针会耗费过多的寄存器。
代码清单14-2 ComputerGravitation_AOS函数(CPU实现)
float
ComputeGravitation_AOS( float \*force, float \*posMass, float softeningSquared, size_t N
1 chTimerTimestamp start, end; chTimerGetTime( &start ); for ( size_t i = 0; i < N; i++) { float ax $=$ 0.0f; float ay $=$ 0.0f; float az $=$ 0.0f; float myX $\equiv$ posMass[i\*4+0]; float myY $\equiv$ posMass[i\*4+1]; float myZ $\equiv$ posMass[i\*4+2]; for ( size_t j = 0; j < N; j++) { float acc[3]; float bodyX $\equiv$ posMass[j\*4+0];float bodyY = posMass[j*4+1];
float bodyZ = posMass[j*4+2];
float bodyMass = posMass[j*4+3];
bodyBodyInteraction<float>(ax, ay, az, myX, myY, myZ, bodyX, bodyY, bodyZ, bodyMass, softeningSquared);
ax += acc[0];
ay += acc[1];
az += acc[2];
}
force[3*i+0] = ax;
force[3*i+1] = ay;
force[3*i+2] = az;
}
chTimerGetTime(&end);
return (float) chTimerElapsedTime(&start, &end) * 1000.0f;代码清单14-3给出了等价于代码清单14-2中函数的GPU版本。对于每一个个体,累计来自其他每一个个体的作用力值,然后将这个累计值写到一个作用力数组中。SM 2.x及以后版本中有一级和二级缓存,可以很好地加速这类负载,因为在内层循环中有大量的数据重用发生。
外层循环和内存循环都会将输入数组变成float4形式,这样可以保证编译器能够正确的发出单条16位加载指令。循环展开是在GPU上进行N-体计算最常用的优化方案,不难想象为什么是这样:GPU上分支结构的开销远大于CPU,因此在每次循环中减少指令能够带来很大好处,而且循环展开能够得到更多指令级并行(instruction level parallelism,ILP)的机会,这样GPU可以隐藏指令执行延时和内存访问延时。
表14-1 简单内核上的循环展开
为了在我们的N-体计算中利用循环展开的优点,我们需要在内层针对j的for循环前插入这行代码:
pragma unroll
不幸的是最佳循环展开因子需要依靠经验决定。表14-1总结了在这个内核中循环展开的效果。
在这个内核案例中,不使用循环展开时,每秒仅能计算个体间的作用力数目为250亿。即使展开因子是2,也能将计算性能提升到了300亿个每秒;将展开因子增加到16时,观测到内核的最佳性能:每秒计算个体间的作用力高达343亿个,得到 的性能提升。
代码清单14-3 ComputeNBodyGravitation_GPU_AOS
template<typename T> global __void
ComputeNBodyGravitation_GPU_AOS()
{
T *force,
T *posMass,
size_t N,
T softeningSquared()
{
for (int i = BlockIdx.x*blockDim.x + threadIdx.x;
i < N;
i += blockDim.x*gridDim.x)
{
T acc[3] = {0};
float4 me = ((float4 *) posMass)[i];
T myX = me.x;
T myY = me.y;
T myZ = me.z;
for (int j = 0; j < N; j++) {
float4 body = ((float4 *) posMass)[j];
float fx, fy, fz;
bodyBodyInteraction(
&fx, &fy, &fz,
myX, myY, myZ,
body.x, body.y, body.z, body.w,
softeningSquared);
acc[0] += fx;
acc[1] += fy;
acc[2] += fz;
}
force[3*i+0] = acc[0];
force[3*i+1] = acc[1];
force[3*i+2] = acc[2];
}[1] 原文误作(fx, fy, fz)。——译者注