ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CUDA共享内存优化实战:从原理到矩阵乘法性能提升

CUDA共享内存优化实战:从原理到矩阵乘法性能提升 这次我们来看一个CUDA性能优化的核心话题共享内存。对于任何想在GPU上榨干最后一点性能的开发者来说共享内存Shared Memory是绕不开的“王牌”技术。它不是简单的内存概念而是CUDA编程中实现线程间高速通信、减少全局内存访问、从而大幅提升并行计算效率的关键武器。如果你正在处理矩阵乘法、图像卷积、归约求和等计算密集型任务并且发现GPU的全局内存带宽成了瓶颈那么深入理解并应用共享内存性能提升一个数量级并非不可能。本文不会停留在概念讲解而是直接切入实战从共享内存的硬件特性、编程模型到如何设计高效的数据复用模式最后通过一个矩阵乘法的经典案例手把手带你写出性能飙升的CUDA内核。本文适合已经具备CUDA C/C基础了解线程、线程块、网格等基本概念并希望将内核性能优化到极致的开发者。我们将重点关注共享内存是什么它在GPU硬件中的位置、速度对比以及容量限制。为什么用它如何通过减少全局内存访问延迟来突破性能瓶颈。怎么用好它包括__shared__关键字、内存体Bank冲突的识别与避免。实战优化以一个分块矩阵乘法为例展示从朴素实现到共享内存优化实现的完整代码演进和性能对比。1. 核心能力速览在深入代码之前我们先快速把握共享内存优化的核心要点能力项说明定位与速度位于每个流多处理器SM芯片上是片上内存。速度比全局内存显存快1-2个数量级延迟极低。容量限制资源有限。不同架构GPU的共享内存大小不同通常为每线程块Block48KB或96KB可配置。设计算法时必须精打细算。访问粒度以内存体Bank为单位组织。32位模式下通常有32个Bank。需避免Bank冲突以保证高带宽。编程接口使用__shared__关键字在核函数内声明。生命周期与线程块相同块内所有线程可读写。核心价值数据复用将频繁访问的全局数据加载到共享内存供块内线程多次快速访问避免重复访问慢速的全局内存。典型应用场景矩阵运算乘、转置、卷积、归约求和、求最值、排序、直方图统计等需要线程间协作或数据重用的算法。硬件门槛支持CUDA的NVIDIA GPU即可。无需特定系列从老架构到最新的50系显卡如RTX 5090原理相通。性能收益优化得当内核性能可提升数倍至数十倍具体取决于原内核的全局内存访问模式。2. 适用场景与使用边界共享内存优化并非银弹它有明确的适用场景和设计边界。适合谁用高性能计算HPC开发者从事科学计算、物理模拟需要极致优化计算内核。AI/ML框架与算法工程师自定义CUDA算子优化卷积、注意力机制等底层操作。图形与视觉工程师编写实时图像处理、计算机视觉算法如滤波、特征提取。任何受限于内存带宽的CUDA程序员当你的内核性能分析如使用Nsight Compute显示“Global Memory Load/Store Efficiency”低下时。能解决什么问题隐藏全局内存延迟通过将数据预取到共享内存后续访问无需经过高延迟的全局内存总线。促进线程协作块内线程可以高效地交换中间计算结果实现复杂的并行算法模式。实现高效的数据复用在诸如矩阵乘法中一个数据元素会被多个线程使用共享内存避免了每个线程都去全局内存读取该数据。不适合什么场景数据复用率低如果每个数据元素只被读取或写入一次使用共享内存只会增加额外的加载/存储开销和同步成本得不偿失。共享内存容量不足如果算法需要缓存的数据集远大于共享内存容量强行分割会导致复杂的代码和可能更多的全局内存访问。初学CUDA尚未理解基本模型建议先掌握全局内存、常量内存、线程同步等基础再挑战共享内存优化。使用边界与注意事项同步是关键在线程块内使用__syncthreads()屏障确保所有线程完成共享内存的写入后再读取。错误同步会导致竞态条件Race Condition和错误结果。Bank冲突是性能杀手需要精心设计数据访问模式避免多个线程同时访问同一个Bank的不同地址Bank Conflict。资源竞争共享内存和L1缓存共享硬件资源在某些架构上可配置。分配更多共享内存可能会减少L1缓存需根据算法特性权衡。3. 环境准备与前置条件在开始编码前请确保你的开发环境已就绪。1. 硬件要求一台配备NVIDIA GPU的电脑。从GeForce游戏卡到Tesla计算卡均可。确保GPU驱动已安装并且支持你将要使用的CUDA版本。2. 软件环境操作系统Windows 10/11, Linux (Ubuntu/CentOS等)或 macOS通过特定方式支持CUDA但推荐前两者。CUDA Toolkit本文示例基于CUDA 11.x或12.x。请从NVIDIA官网下载并安装对应版本的CUDA Toolkit。编译器Windows下使用Visual Studio对应CUDA版本的VS工具集Linux下使用gcc/g。IDE/编辑器推荐使用Visual StudioWindows或VSCode配合CUDA插件方便代码编写和调试。3. 验证环境打开终端或命令提示符运行以下命令检查CUDA安装# 检查CUDA编译器版本 nvcc --version # 查看GPU信息 nvidia-smi确保nvcc命令可用并且nvidia-smi能正确显示你的GPU型号、驱动版本和CUDA版本。4. 性能分析工具可选但强烈推荐Nsight Systems用于系统级性能分析。Nsight Compute用于内核级性能分析可以详细查看共享内存效率、Bank冲突等指标。 安装这些工具能帮助你量化优化效果精准定位瓶颈。4. 共享内存编程基础4.1 声明与生命周期在CUDA核函数__global__函数内部使用__shared__关键字声明共享内存变量。__global__ void myKernel(float* input, float* output) { // 声明一个静态大小的共享内存数组 __shared__ float s_data[256]; // 或者声明一个动态大小的共享内存数组内核启动时指定大小 extern __shared__ float s_dynamic_data[]; // 每个线程根据自己的线程ID (threadIdx.x) 将全局内存数据加载到共享内存 int tid threadIdx.x; s_data[tid] input[blockIdx.x * blockDim.x tid]; // 确保块内所有线程都完成了数据加载 __syncthreads(); // ... 后续使用 s_data 进行计算 ... }静态声明大小在编译时确定如s_data[256]。动态声明使用extern __shared__大小在内核启动时通过第三个执行配置参数指定例如myKernelgrid, block, sharedMemSize(...)。生命周期共享内存变量在该线程块Block的整个生命周期内存在。当线程块执行完毕其分配的共享内存被释放。不同线程块之间的共享内存是隔离的。4.2 线程同步__syncthreads()这是共享内存编程中至关重要的一步。它确保一个线程块中的所有线程都执行到这个屏障点后才继续向下执行。在数据加载后同步确保所有线程都已将所需数据写入共享内存其他线程才能安全读取。在数据写入共享内存前同步有时需要确保共享内存中的旧数据已被所有线程使用完毕然后再写入新数据。忘记同步或同步位置错误是导致计算结果随机错误的常见原因。4.3 内存体Bank与Bank冲突共享内存被组织成多个大小相等的内存体Bank以便同时服务多个访问请求。对于计算能力5.0及以上如Pascal, Volta, Ampere, Ada Lovelace架构默认是32位模式4字节宽有32个Bank。理想情况当块内多个线程同时访问共享内存时如果它们访问的地址位于不同的Bank或同一个Bank的相同地址广播那么这些访问可以在一个时钟周期内并行完成。Bank冲突如果多个线程访问同一个Bank的不同地址这些访问会被序列化导致性能急剧下降。冲突的线程数越多序列化的周期数越多。如何避免Bank冲突改变数据布局例如在矩阵转置中按列访问可能导致Bank冲突可以通过填充Padding来改变内存地址到Bank的映射。使用更宽的数据类型例如使用float28字节代替两个float4字节访问在特定条件下可能改变Bank对齐方式。调整线程访问模式设计算法时让线程的访问地址尽量 stride 为奇数或与Bank数量互质。5. 实战优化从朴素矩阵乘法到共享内存优化我们以计算 $C A \times B$假设矩阵维度为MxN (MxK) * (KxN)为例展示优化过程。假设矩阵按行主序存储。5.1 版本一朴素全局内存访问这是最直接的实现每个线程计算输出矩阵C的一个元素。它需要从全局内存中读取A的一整行和B的一整列导致大量的全局内存访问且没有数据复用。__global__ void naiveMatMul(float* A, float* B, float* C, int M, int N, int K) { int row blockIdx.y * blockDim.y threadIdx.y; int col blockIdx.x * blockDim.x threadIdx.x; if (row M col N) { float sum 0.0f; for (int k 0; k K; k) { // 每次循环都要访问全局内存效率低下 sum A[row * K k] * B[k * N col]; } C[row * N col] sum; } } // 启动配置示例: dim3 block(16, 16); dim3 grid((N15)/16, (M15)/16);性能问题每个线程进行K次循环每次循环读取两个全局内存值A和B各一个。总共M*N个线程全局内存访问次数约为2 * M * N * K。而且对矩阵B的访问是列主序的导致非合并访问Uncoalesced Access进一步降低带宽利用率。5.2 版本二引入共享内存分块Tiling核心思想将矩阵A和B分成小块Tile每个线程块负责计算C的一个小块。线程块先将所需的小块A和B从全局内存加载到共享内存中然后块内线程从快速的共享内存中读取数据进行计算。这样每个全局内存中的数据元素被加载一次然后在共享内存中被多个线程复用。假设我们定义块大小为BLOCK_SIZE例如16。#define BLOCK_SIZE 16 __global__ void tiledMatMul(float* A, float* B, float* C, int M, int N, int K) { // 为当前线程块声明共享内存用于存储A和B的一个分块 __shared__ float As[BLOCK_SIZE][BLOCK_SIZE]; __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE]; // 线程块负责计算C中哪个分块 int bx blockIdx.x; int by blockIdx.y; // 线程在线程块内的索引 int tx threadIdx.x; int ty threadIdx.y; // 计算该线程负责的C中的元素坐标全局坐标 int row by * BLOCK_SIZE ty; int col bx * BLOCK_SIZE tx; float sum 0.0f; // 循环遍历K维度上的分块 for (int ph 0; ph (K BLOCK_SIZE - 1) / BLOCK_SIZE; ph) { // 协作加载A的分块到共享内存 int loadA_row row; int loadA_col ph * BLOCK_SIZE tx; if (loadA_row M loadA_col K) { As[ty][tx] A[loadA_row * K loadA_col]; } else { As[ty][tx] 0.0f; } // 协作加载B的分块到共享内存 int loadB_row ph * BLOCK_SIZE ty; int loadB_col col; if (loadB_row K loadB_col N) { Bs[ty][tx] B[loadB_row * N loadB_col]; } else { Bs[ty][tx] 0.0f; } // 等待块内所有线程完成共享内存的加载 __syncthreads(); // 从共享内存中读取数据进行计算 for (int k 0; k BLOCK_SIZE; k) { sum As[ty][k] * Bs[k][tx]; } // 在加载下一个分块前确保所有线程已完成当前分块的计算 __syncthreads(); } // 将最终结果写回全局内存C if (row M col N) { C[row * N col] sum; } }优化解析分块Tiling将大的矩阵乘法分解为多个BLOCK_SIZE x BLOCK_SIZE的小矩阵乘法。协作加载线程块内的所有线程协作将计算一个小块C所需的A和B的对应小块从全局内存加载到共享内存As和Bs中。数据复用加载到As和Bs中的数据在内部k循环中被每个线程重复访问BLOCK_SIZE次。这避免了从全局内存重复读取。双重同步第一个__syncthreads()确保数据加载完成后再开始计算。第二个__syncthreads()确保所有线程用完当前共享内存块的数据后再加载下一块防止数据被覆盖。5.3 版本三优化共享内存访问模式避免Bank冲突在版本二中As[ty][k]和Bs[k][tx]的访问模式可能存在Bank冲突。我们以BLOCK_SIZE16为例分析As是[BLOCK_SIZE][BLOCK_SIZE]的二维数组在内存中是按行连续存储的。当线程ty固定k从0到15循环读取As[ty][k]时同一warp内的线程假设ty相同tx不同访问的是同一行的不同列。在32个Bank、4字节宽度的模式下如果BLOCK_SIZE是16或32的倍数同一行的连续元素可能映射到相同的Bank导致Bank冲突16-way冲突。解决方案使用填充Padding通过将共享内存数组的宽度增加一个元素即声明为[BLOCK_SIZE][BLOCK_SIZE1]可以改变列元素到Bank的映射关系从而避免大多数Bank冲突。#define BLOCK_SIZE 16 __global__ void tiledMatMulNoBankConflict(float* A, float* B, float* C, int M, int N, int K) { // 使用填充来避免Bank冲突 __shared__ float As[BLOCK_SIZE][BLOCK_SIZE1]; // 注意 1 __shared__ float Bs[BLOCK_SIZE][BLOCK_SIZE1]; // 注意 1 int bx blockIdx.x; int by blockIdx.y; int tx threadIdx.x; int ty threadIdx.y; int row by * BLOCK_SIZE ty; int col bx * BLOCK_SIZE tx; float sum 0.0f; for (int ph 0; ph (K BLOCK_SIZE - 1) / BLOCK_SIZE; ph) { // 加载数据到填充后的共享内存 int loadA_col ph * BLOCK_SIZE tx; if (row M loadA_col K) { As[ty][tx] A[row * K loadA_col]; // 按行加载ty行tx列 } else { As[ty][tx] 0.0f; } int loadB_row ph * BLOCK_SIZE ty; if (loadB_row K col N) { Bs[ty][tx] B[loadB_row * N col]; // 注意这里Bs的加载模式 } else { Bs[ty][tx] 0.0f; } __syncthreads(); // 计算时访问模式不变但由于填充Bank冲突被消除或减轻 for (int k 0; k BLOCK_SIZE; k) { // As按行存储As[ty][k]是连续访问无冲突。 // Bs按列加载但存储时也是行优先。Bs[k][tx]是跨行访问。 // 由于填充跨行访问的地址偏移是 (BLOCK_SIZE1)与Bank数量32互质避免了冲突。 sum As[ty][k] * Bs[k][tx]; } __syncthreads(); } if (row M col N) { C[row * N col] sum; } }关键点BLOCK_SIZE1的填充使得同一列中相邻行的元素在内存地址上相差BLOCK_SIZE1个元素。如果BLOCK_SIZE1是奇数或者与Bank数量32互质那么这些元素就会落在不同的Bank中从而避免了Bank冲突。6. 性能测试与效果验证6.1 测试环境搭建编写一个简单的主机Host代码来驱动上述内核并测量运行时间。#include iostream #include cuda_runtime.h #include device_launch_parameters.h #include chrono // 这里插入上面三个核函数的定义 (naiveMatMul, tiledMatMul, tiledMatMulNoBankConflict) void matMulCPU(float* A, float* B, float* C, int M, int N, int K) { for (int i 0; i M; i) { for (int j 0; j N; j) { float sum 0.0f; for (int k 0; k K; k) { sum A[i * K k] * B[k * N j]; } C[i * N j] sum; } } } int main() { int M 1024, N 1024, K 1024; // 测试1024x1024矩阵 size_t sizeA M * K * sizeof(float); size_t sizeB K * N * sizeof(float); size_t sizeC M * N * sizeof(float); // 分配主机内存并初始化 float *h_A new float[M*K]; float *h_B new float[K*N]; float *h_C new float[M*N]; float *h_C_ref new float[M*N]; // ... 初始化 h_A, h_B ... // 分配设备内存 float *d_A, *d_B, *d_C; cudaMalloc(d_A, sizeA); cudaMalloc(d_B, sizeB); cudaMalloc(d_C, sizeC); // 拷贝数据到设备 cudaMemcpy(d_A, h_A, sizeA, cudaMemcpyHostToDevice); cudaMemcpy(d_B, h_B, sizeB, cudaMemcpyHostToDevice); // 定义线程块和网格大小 dim3 block(BLOCK_SIZE, BLOCK_SIZE); dim3 grid((N block.x - 1) / block.x, (M block.y - 1) / block.y); // 预热 tiledMatMulNoBankConflictgrid, block(d_A, d_B, d_C, M, N, K); cudaDeviceSynchronize(); // 计时开始 auto start std::chrono::high_resolution_clock::now(); for (int i 0; i 100; i) { // 多次运行取平均 tiledMatMulNoBankConflictgrid, block(d_A, d_B, d_C, M, N, K); } cudaDeviceSynchronize(); auto end std::chrono::high_resolution_clock::now(); std::chrono::durationdouble elapsed end - start; std::cout 优化版内核平均耗时: elapsed.count() / 100 * 1000 ms std::endl; // 拷贝结果回主机并验证正确性 (与CPU结果或朴素GPU结果对比) cudaMemcpy(h_C, d_C, sizeC, cudaMemcpyDeviceToHost); matMulCPU(h_A, h_B, h_C_ref, M, N, K); // ... 验证 h_C 与 h_C_ref 的误差 ... // 清理 delete[] h_A; delete[] h_B; delete[] h_C; delete[] h_C_ref; cudaFree(d_A); cudaFree(d_B); cudaFree(d_C); return 0; }编译命令示例Linuxnvcc -o matmul_benchmark matmul_benchmark.cu -stdc11 ./matmul_benchmark6.2 预期性能对比在RTX 4060或其他中端GPU上测试1024x1024的矩阵乘法你可能会观察到类似以下的趋势具体倍数因硬件和参数而异朴素版本 (naiveMatMul)基准性能可能只有几十到几百 GFLOPs。分块共享内存版本 (tiledMatMul)性能可能有3-8倍的提升。主要收益来自于数据复用和更好的全局内存合并访问。避免Bank冲突版本 (tiledMatMulNoBankConflict)在BLOCK_SIZE较大如32时可能还会带来10%-30%的额外性能提升。对于BLOCK_SIZE16提升可能不明显因为冲突程度较轻。使用Nsight Compute验证运行ncu -o profile ./matmul_benchmark生成性能报告。在报告中关注以下指标sm__throughput.avg.pct_of_peak_sustained_elapsedSM吞吐量利用率。l1tex__t_sectors_pipe_lsu_mem_global_op_ld.sum和.st.sum全局内存加载/存储请求数。优化后应显著减少。l1tex__data_pipe_lsu_wavefronts_mem_shared_op_ld.sum共享内存加载操作。优化后应成为主要内存操作。l1tex__data_bank_conflicts_pipe_lsu_mem_shared.sum共享内存Bank冲突次数。优化版本三应比版本二低。7. 资源占用与性能观察共享内存优化直接影响GPU核心的资源利用和性能表现。1. 显存全局内存占用优化本身不减少输入输出矩阵的显存占用。但它通过减少对全局内存的访问次数极大降低了显存带宽压力。这是性能提升的根本原因。2. 共享内存占用每个线程块占用2 * BLOCK_SIZE * BLOCK_SIZE * sizeof(float)字节的共享内存如果使用填充则是2 * BLOCK_SIZE * (BLOCK_SIZE1) * sizeof(float)。例如BLOCK_SIZE16float为4字节无填充2*16*16*4 2048字节 2KB。有填充2*16*17*4 2176字节 ≈ 2.12KB。GPU上每个SM的共享内存总量有限如48KB。这限制了每个SM上能同时驻留的线程块数量从而影响Occupancy占用率。需要权衡块大小和占用率。3. 占用率Occupancy观察使用Nsight Compute的Occupancy模型。增加BLOCK_SIZE会增加每个块的线程数可能提高计算粒度。增加每个块的共享内存使用量可能降低SM上同时活跃的线程块数量从而可能降低占用率。最优的BLOCK_SIZE需要实验确定通常是16或32。现代GPU如Ampere的共享内存容量更大可以支持更大的块或更高的占用率。4. 性能瓶颈转移优化前瓶颈在全局内存带宽。优化后瓶颈可能转移到计算单元ALU吞吐量或指令发射上。此时可以进一步探索循环展开#pragma unroll。使用向量化加载如float2,float4。利用张量核心Tensor Cores但这需要改用mma指令或库如cuBLAS。8. 常见问题与排查方法问题现象可能原因排查方式解决方案内核运行结果不正确NaN或错误值1. 共享内存未初始化或越界访问。2.__syncthreads()使用错误或遗漏。3. 线程索引计算错误导致加载了错误的数据。1. 使用cuda-memcheck检查内存错误。2. 在内核中添加printf调试仅限少量线程打印共享内存加载的值和索引。3. 编写一个极小的测试用例如4x4矩阵进行逐步验证。1. 确保每个共享内存位置都被正确赋值边界线程填充0。2. 仔细检查每个数据加载阶段和计算阶段后的__syncthreads()是否必要且位置正确。3. 反复核对row,col,loadA_row,loadA_col等索引计算公式。优化后性能反而下降1.BLOCK_SIZE选择不当导致占用率过低。2. 引入了严重的Bank冲突。3. 共享内存使用量过大限制了并发线程块数量。1. 使用Nsight Compute查看Occupancy和Active Warps。2. 查看l1tex__data_bank_conflicts_pipe_lsu_mem_shared.sum指标。3. 计算每个块的共享内存使用量和每个SM的理论最大线程块数。1. 尝试不同的BLOCK_SIZE如8, 16, 32。2. 使用填充或改变数据布局来避免Bank冲突。3. 减少每个块的共享内存使用量或尝试调整CUDA内核启动配置。内核启动失败返回cudaErrorInvalidValue每个块请求的动态共享内存大小超过了设备限制。使用cudaGetDeviceProperties查询sharedMemPerBlock和sharedMemPerMultiprocessor。减少动态共享内存的请求大小或改用静态共享内存并减小数组尺寸。无法达到理论峰值性能1. 算法本身计算强度Compute Intensity不足。2. 仍有未优化的全局内存访问如非合并访问。3. 指令开销大循环效率低。1. 使用Nsight Compute的Roofline模型分析。2. 检查全局内存加载/存储效率指标。3. 查看反汇编代码或SASS指令统计。1. 考虑进一步增加数据复用如使用寄存器缓存。2. 确保全局内存访问是合并的。3. 使用循环展开、预取等技术。不同GPU架构上性能差异大不同架构的共享内存大小、Bank数量、L1/共享内存配置比例不同。查阅NVIDIA官方文档对应架构的编程指南。针对特定架构进行微调例如在Ampere架构上可以尝试调整L1/共享内存缓存配置cudaFuncSetAttribute。9. 最佳实践与使用建议Profile First先分析不要盲目使用共享内存。先用性能分析工具如Nsight Compute确定你的内核瓶颈确实是全局内存带宽。从小开始逐步验证先实现一个能正确工作的朴素版本然后逐步添加共享内存优化每一步都验证结果的正确性。精心设计数据加载模式确保从全局内存到共享内存的加载是合并访问的。这通常意味着让一个warp内的线程访问连续的内存地址。同步是生命线在共享内存写入后和读取前务必使用__syncthreads()。仔细考虑同步屏障的位置。警惕Bank冲突对于性能关键的内核使用Nsight Compute检查Bank冲突。如果冲突严重尝试使用填充或改变数组维度来缓解。平衡占用率与数据复用更大的分块BLOCK_SIZE意味着更高的数据复用但也会消耗更多共享内存和寄存器可能降低占用率。需要通过实验找到最佳平衡点。考虑使用只读缓存Read-Only Cache对于某些只读的全局数据CUDA还提供了通过__ldg()指令或const __restrict__修饰符使用只读缓存L1/Texture Cache的途径有时这可能比手动管理共享内存更简单有效。了解硬件特性熟悉你目标GPU的架构特性如Maxwell, Pascal, Volta, Turing, Ampere, Ada Lovelace不同架构的共享内存、寄存器文件、缓存层次都有差异。10. 总结与下一步共享内存是CUDA性能优化工具箱中最锋利的一把刀。它通过将数据缓存在芯片上的高速内存中让线程块内的协作计算变得极其高效从而彻底释放GPU的并行计算潜力。核心要点可以归结为分块加载、协作共享、同步有序、避免冲突。对于矩阵乘法这个例子从朴素版本到共享内存优化版本性能的提升是颠覆性的。但这仅仅是开始。你可以在此基础上继续深入探索更大的分块和寄存器缓存在共享内存中缓存数据的基础上进一步利用线程的私有寄存器来缓存共享内存中的数据减少对共享内存的访问。使用向量化加载使用float2或float4一次加载更多数据提高全局内存带宽利用率。迈向张量核心对于支持Tensor Core的GPUVolta及以后学习使用WMMAWarp Matrix Multiply AccumulateAPI或直接调用cuBLAS的矩阵乘法以利用硬件提供的极致算力。将模式应用到其他算法卷积、归约、排序、FFT等算法都有经典的共享内存优化方案其思想是相通的。建议你将本文的代码作为模板在自己的GPU上实际运行和 profiling观察不同BLOCK_SIZE、有无填充对性能的具体影响。只有亲手实践和测量才能真正掌握这门“榨干GPU性能”的艺术。
返回列表