1. 并行规约从基础实现到性能瓶颈分析并行规约是GPU编程中最经典的优化案例之一它完美展示了如何通过逐步优化将一个简单算法推向性能极限。我第一次接触这个算法时被它从毫秒级优化到微秒级的过程深深震撼。让我们从一个最简单的实现开始看看它存在哪些性能问题。最基本的相邻配对规约算法思路很简单将数组元素两两相加结果存回前一个位置如此反复直到剩下最后一个元素。在CUDA中我们可以用共享内存来加速这个过程__global__ void reduce0(float* g_in, float* g_out) { __shared__ float s_data[BLOCK_SIZE]; int tid threadIdx.x; s_data[tid] g_in[blockIdx.x*blockDim.x tid]; __syncthreads(); for(int s1; sblockDim.x; s*2) { if(tid % (2*s) 0) { s_data[tid] s_data[tid s]; } __syncthreads(); } if(tid 0) g_out[blockIdx.x] s_data[0]; }这个实现看起来合理但实际性能却非常糟糕。在我的RTX 3080上测试32M数据的规约耗时约5.7ms加速比只有15倍左右。为什么这么慢通过Nsight工具分析发现存在两个主要问题首先是线程束分化Warp Divergence。在if条件判断时一个warp中只有部分线程满足条件执行加法其他线程必须等待。这种分支差异会导致严重的性能下降。想象一下32个人的团队每次只有部分人在工作其他人站着等效率自然低下。其次是共享内存的bank冲突。当多个线程同时访问同一个共享内存bank时这些访问会变成串行执行。在我们的实现中相邻线程访问相邻内存地址正好触发了最严重的bank冲突情况。2. 消除线程束分化的优化技巧解决线程束分化的关键在于重构算法让一个warp中的所有线程执行相同的指令。对于规约算法我们可以改变数据访问模式__global__ void reduce1(float* g_in, float* g_out) { __shared__ float s_data[BLOCK_SIZE]; int tid threadIdx.x; s_data[tid] g_in[blockIdx.x*blockDim.x tid]; __syncthreads(); for(int s1; sblockDim.x; s*2) { int index 2*s*tid; if(index blockDim.x) { s_data[index] s_data[index s]; } __syncthreads(); } if(tid 0) g_out[blockIdx.x] s_data[0]; }这个版本通过重新映射线程索引确保一个warp中的所有线程要么都执行加法要么都不执行。理论上这应该提升性能但实测结果却出人意料——性能反而下降了从5.7ms增加到6.9ms。这个反直觉的结果其实揭示了另一个隐藏问题bank冲突。虽然消除了线程束分化但新的访问模式导致了更严重的bank冲突。每个warp中的线程现在访问的内存地址间隔更大更容易碰撞到同一个bank。3. 解决Bank Conflict的实战方法要解决bank冲突我们需要理解共享内存的组织方式。共享内存被分为32个bank对应一个warp的32个线程连续的32-bit字被分配到连续的bank。当多个线程同时访问同一个bank的不同地址时就会发生bank冲突。交错规约是一种同时解决线程束分化和bank冲突的优雅方案__global__ void reduce2(float* g_in, float* g_out) { __shared__ float s_data[BLOCK_SIZE]; int tid threadIdx.x; s_data[tid] g_in[blockIdx.x*blockDim.x tid]; __syncthreads(); for(int sblockDim.x/2; s0; s1) { if(tid s) { s_data[tid] s_data[tid s]; } __syncthreads(); } if(tid 0) g_out[blockIdx.x] s_data[0]; }这个版本有两个关键改进从大跨度开始规约逐步缩小跨度只使用前一半线程工作后一半线程闲置实测性能提升到4.7ms加速比达到18倍。为什么这种模式能避免bank冲突因为现在活跃线程访问的共享内存地址间隔很大不太可能落在同一个bank中。4. 高级优化技巧加载时计算与循环展开进一步优化可以从两个方向入手提高内存访问效率和减少指令开销。加载时计算Load-time Computation让每个线程处理更多数据__global__ void reduce3(float* g_in, float* g_out) { __shared__ float s_data[BLOCK_SIZE]; int tid threadIdx.x; int i blockIdx.x*(2*blockDim.x) tid; s_data[tid] g_in[i] g_in[iblockDim.x]; __syncthreads(); for(int sblockDim.x/2; s0; s1) { if(tid s) { s_data[tid] s_data[tid s]; } __syncthreads(); } if(tid 0) g_out[blockIdx.x] s_data[0]; }这个版本让线程数量减半每个线程加载两个数据并预先求和。这更好地隐藏了内存访问延迟性能提升到3.6ms。循环展开是另一个重要技巧。当剩余元素不超过一个warp大小时可以手动展开循环__global__ void reduce4(float* g_in, float* g_out) { __shared__ float s_data[BLOCK_SIZE]; int tid threadIdx.x; int i blockIdx.x*(2*blockDim.x) tid; s_data[tid] g_in[i] g_in[iblockDim.x]; __syncthreads(); for(int sblockDim.x/2; s32; s1) { if(tid s) s_data[tid] s_data[tid s]; __syncthreads(); } if(tid 32) { volatile float* vsdata s_data; vsdata[tid] vsdata[tid 32]; vsdata[tid] vsdata[tid 16]; vsdata[tid] vsdata[tid 8]; vsdata[tid] vsdata[tid 4]; vsdata[tid] vsdata[tid 2]; vsdata[tid] vsdata[tid 1]; } if(tid 0) g_out[blockIdx.x] s_data[0]; }使用volatile关键字确保编译器不会优化掉这些看似冗余的访问。这个版本性能大幅提升到1.9ms加速比达到惊人的46倍5. 终极优化完全展开与Shuffle指令对于固定大小的block可以完全展开循环。模板函数让编译器在编译时展开所有循环template int blockSize __global__ void reduce5(float* g_in, float* g_out) { __shared__ float s_data[blockSize]; int tid threadIdx.x; int i blockIdx.x*(2*blockDim.x) tid; s_data[tid] g_in[i] g_in[iblockDim.x]; __syncthreads(); if(blockSize 1024) { if(tid 512) s_data[tid] s_data[tid512]; __syncthreads(); } if(blockSize 512) { if(tid 256) s_data[tid] s_data[tid256]; __syncthreads(); } if(blockSize 256) { if(tid 128) s_data[tid] s_data[tid128]; __syncthreads(); } if(blockSize 128) { if(tid 64) s_data[tid] s_data[tid64]; __syncthreads(); } if(tid 32) { volatile float* vsdata s_data; vsdata[tid] vsdata[tid 32]; vsdata[tid] vsdata[tid 16]; vsdata[tid] vsdata[tid 8]; vsdata[tid] vsdata[tid 4]; vsdata[tid] vsdata[tid 2]; vsdata[tid] vsdata[tid 1]; } if(tid 0) g_out[blockIdx.x] s_data[0]; }现代GPU还提供了shuffle指令允许线程直接访问同warp内其他线程的寄存器template int blockSize __device__ float warpReduce(float val) { for(int offset16; offset0; offset1) val __shfl_down_sync(0xffffffff, val, offset); return val; } template int blockSize __global__ void reduce6(float* g_in, float* g_out) { float sum 0; for(int iblockIdx.x*blockDim.x threadIdx.x; iN; iblockDim.x*gridDim.x) sum g_in[i]; sum warpReduceblockSize(sum); if((blockSize 32) (threadIdx.x 32)) { __shared__ float warpSum[32]; int lane threadIdx.x % 32; int wid threadIdx.x / 32; if(lane 0) warpSum[wid] sum; __syncthreads(); sum (threadIdx.x (blockDim.x / 32)) ? warpSum[lane] : 0; if(wid 0) sum warpReduceblockSize/32(sum); } if(threadIdx.x 0) g_out[blockIdx.x] sum; }shuffle版本避免了共享内存的使用在某些情况下性能更好。在我的测试中最优版本达到了1.01ms加速比超过90倍。从最初的5.7ms到1.01ms近6倍的性能提升展示了CUDA优化的巨大潜力。