以下是针对 2.2.4 Memory Performance 和 2.2.4.1 Coalesced Global Memory Access 章节内容设计的完整知识点巩固习题包。
内存性能与合并全局内存访问知识巩固习题包
一、选择题(每题只有一个正确答案)
1. 全局内存通过多大尺寸的内存事务进行访问?
A. 16字节
B. 32字节
C. 64字节
D. 128字节
2. 如果一个warp中的每个线程请求4字节数据,warp总共需要多少字节的数据?
A. 32字节
B. 64字节
C. 128字节
D. 256字节
3. 在完美合并访问的情况下,一个warp的128字节数据请求需要多少个32字节的内存事务?
A. 1个
B. 2个
C. 4个
D. 8个
4. 在最坏情况的未合并访问中,内存利用率大约是多少?
A. 50%
B. 25%
C. 12.5%
D. 6.25%
5. 关于合并内存访问,下列说法正确的是?
A. 连续线程必须访问连续内存地址才能实现合并
B. 合并访问只影响读操作,不影响写操作
C. 合并访问的关键是让warp内线程访问的数据来自相同的32字节内存段
D. 合并访问只对float类型有效
6. 在矩阵转置示例中,读操作a[INDX(myRow, myCol, m)]的合并情况如何?
A. 完全未合并
B. 部分合并
C. 完全合并
D. 取决于矩阵大小
7. 在矩阵转置示例中,写操作c[INDX(myCol, myRow, m)]的合并情况如何?
A. 完全合并
B. 未合并(步长为ld)
C. 取决于ld是否为32的倍数
D. 完全合并,因为所有线程同时写入
8. 如果一个warp中32个线程分别访问地址0, 4, 8, ..., 124,内存利用率是多少?
A. 12.5%
B. 50%
C. 75%
D. 100%
9. 如果一个warp中32个线程分别访问地址0, 32, 64, ..., 992,内存利用率是多少?
A. 12.5%
B. 25%
C. 50%
D. 100%
10. 在2D线程块中,哪个维度的线程索引变化最快?
A. x维度
B. y维度
C. z维度
D. 取决于编译器
11. 关于内存事务和合并访问,下列说法错误的是?
A. 每个内存事务获取32字节连续数据
B. 合并访问可以减少内存事务数量
C. 未合并访问会导致更多内存事务
D. 内存事务大小可以动态调整
12. 在vecAdd内核示例中,实现合并访问的关键是什么?
A. 使用1D线程块
B. 让workIndex连续的线程访问连续数组元素
C. 使用共享内存
D. 使用常量内存
13. 如果矩阵转置中ld=1024,写操作中连续线程访问的地址间隔是多少字节?
A. 4字节
B. 32字节
C. 1024字节
D. 4096字节
14. 关于合并访问的优化目标,下列说法正确的是?
A. 最小化内存事务数量
B. 最大化已用字节数/传输字节数的比例
C. 最小化每个线程的访问次数
D. 最大化每个内存事务的大小
15. 以下哪种情况最容易实现合并访问?
A. 线程访问随机地址
B. 线程访问步长为32字节的地址
C. 连续线程访问连续4字节地址
D. 所有线程访问同一个地址
二、填空题
**1. 全局内存通过 _____ 字节的内存事务进行访问。
**2. 一个warp由 _____ 个线程组成。
**3. 如果每个线程请求4字节数据,一个warp总共需要 _____ 字节数据。
**4. 在完美合并访问情况下,需要 _____ 个32字节内存事务。
**5. 在最坏未合并访问情况下,需要 _____ 个32字节内存事务,内存利用率为 _____。
**6. 合并访问的关键是让warp内线程访问的数据来自相同的 _____ 内存段。
7. 在2D线程块中,_____** 维度的线程索引变化最快。
**8. 在矩阵转置示例中,读操作 _____ (是/否)实现合并,写操作 _____(是/否)实现合并。
**9. 优化内存访问的目标是最大化 _____ 的比例。
**10. 在vecAdd内核中,workIndex = threadIdx.x + blockIdx.x * blockDim.x确保了连续线程访问 _____ 数组元素。
**11. 如果连续线程访问间隔32字节的地址,每个线程触发 _____ 个独立内存事务。
**12. 矩阵转置示例中,写操作未合并的原因是myCol作为INDX宏的 _____ 参数。
**13. 当ld大于 _____ 时,矩阵转置的写操作会出现类似图11的病态情况。
**14. 解决矩阵转置未合并写入的常用方法是使用 _____ 内存。
**15. 确保全局内存访问的正确合并,是编写高性能CUDA内核 最重要的性能考虑因素 之一。
三、简答题
1. 解释什么是合并内存访问(Coalesced Memory Access),为什么它对性能至关重要?
2. 描述一个warp的内存请求如何被合并为内存事务,以4字节数据请求为例说明。
3. 比较完美合并访问和最坏未合并访问的内存利用率,并说明造成巨大差异的原因。
4. 在矩阵转置示例中,为什么读操作是合并的而写操作是不合并的?
5. 给出实现合并访问的最直接方法,并举例说明。
6. 解释"最大化已用字节数/传输字节数的比例"这一优化目标的含义。
7. 如果矩阵转置的ld=64,写操作中连续线程访问的地址间隔是多少字节?对内存利用率有何影响?
8. 说明在2D线程块中,为什么threadIdx.x变化最快这一特性对内存访问很重要?
9. 为什么说确保合并访问是编写高性能CUDA内核最重要的性能考虑因素之一?
10. 除了连续线程访问连续元素外,还有哪些方式可以实现合并访问?
四、分析题
1. 分析以下内核的内存访问模式,判断读操作和写操作是否合并:
cpp
__global__ void kernel1(float* A, float* B, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
int idx2 = idx * 2;
if (idx2 < N) {
A[idx2] = B[idx2];
}
}
2. 分析以下2D内核的内存访问模式:
cpp
__global__ void kernel2D(float* matrix, int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && y < height) {
int index = y * width + x; // 行优先
matrix[index] *= 2.0f;
}
}
问题:
- 连续线程(threadIdx.x连续)访问的地址是否连续?
- 这种访问模式是否合并?
- 如果改成
index = x * height + y(列优先),访问模式会有什么变化?
3. 分析以下不同的访问模式,计算内存利用率:
cpp
// 模式A
int idx = threadIdx.x;
data[idx] = value;
// 模式B
int idx = threadIdx.x * 2;
data[idx] = value;
// 模式C
int idx = threadIdx.x * 32;
data[idx] = value;
// 模式D
int idx = threadIdx.x + blockIdx.x * blockDim.x;
data[idx] = value;
假设:
- 每个线程写入4字节
- warp大小为32
- 所有模式中连续线程的threadIdx.x连续
计算每种模式:
- 需要的总数据量(字节)
- 触发的内存事务数
- 内存利用率
4. 分析以下矩阵转置变体,比较其内存访问模式:
cpp
// 变体A:原始版本
#define INDX(row, col, ld) ((row)*(ld) + (col))
c[INDX(myCol, myRow, m)] = a[INDX(myRow, myCol, m)];
// 变体B:使用列优先存储结果
#define INDX_COL(row, col, ld) ((col)*(ld) + (row))
c[INDX_COL(myCol, myRow, m)] = a[INDX(myRow, myCol, m)];
问题:
- 变体B中写操作是否变为合并访问?
- 这种改变有什么代价?
- 在实际应用中,如何选择存储布局?
5. 分析以下代码的内存访问模式优化问题:
cpp
__global__ void processStruct(MyStruct* data, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
// 访问结构体的不同成员
float a = data[idx].a;
float b = data[idx].b;
float c = data[idx].c;
data[idx].a = a * 2.0f;
data[idx].b = b * 3.0f;
data[idx].c = c * 4.0f;
}
}
假设MyStruct定义如下:
cpp
struct MyStruct {
float a;
float b;
float c;
};
问题:
- 这种访问模式是否合并?
- 连续线程访问的地址模式是怎样的?
- 如何优化以提高内存访问效率?
- 如果改成数组结构(SOA)布局会怎样?
五、编程练习题
题目1:矩阵转置性能对比
编写一个CUDA程序,实现三种不同版本的矩阵转置,并对比它们的性能:
- 朴素版本:使用全局内存的直接转置(如2.2.4.1.1节所示)
- 优化读版本:优化读操作,但写操作仍然未合并
- 完全优化版本:使用共享内存实现读写都合并的转置
要求:
- 矩阵大小为4096×4096的float类型
- 使用2D线程块,块大小为16×16或32×32
- 测量并对比三种版本的内核执行时间
- 验证所有版本的计算结果正确性
代码框架:
cpp
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#define INDX(row, col, ld) ((row)*(ld) + (col))
// 版本1:朴素转置(读合并,写未合并)
__global__ void transpose_naive(float* input, float* output, int n) {
int col = blockIdx.x * blockDim.x + threadIdx.x;
int row = blockIdx.y * blockDim.y + threadIdx.y;
if (row < n && col < n) {
// TODO: 实现朴素转置
}
}
// 版本2:优化读版本(可能交换块/网格维度)
__global__ void transpose_opt_read(float* input, float* output, int n) {
// TODO: 尝试通过交换blockIdx和gridIdx的角色来优化
// 让写操作变为合并,但读操作可能变差
}
// 版本3:使用共享内存的优化版本(预习下一节)
__global__ void transpose_shared(float* input, float* output, int n) {
__shared__ float tile[BLOCK_SIZE][BLOCK_SIZE];
// TODO: 实现共享内存转置
// 1. 协作加载数据到共享内存
// 2. 同步
// 3. 转置后写回全局内存
}
// 验证函数
bool verifyTranspose(float* original, float* transposed, int n) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (transposed[j * n + i] != original[i * n + j]) {
printf("Mismatch at (%d,%d): %f vs %f\n",
i, j, transposed[j*n+i], original[i*n+j]);
return false;
}
}
}
return true;
}
int main() {
int n = 4096;
size_t size = n * n * sizeof(float);
// 分配主机内存
float* h_input = (float*)malloc(size);
float* h_output = (float*)malloc(size);
// 初始化输入矩阵
for (int i = 0; i < n * n; i++) {
h_input[i] = (float)i;
}
// 分配设备内存
float *d_input, *d_output;
cudaMalloc(&d_input, size);
cudaMalloc(&d_output, size);
cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
// TODO: 配置内核启动参数
dim3 block(BLOCK_SIZE, BLOCK_SIZE);
dim3 grid((n + BLOCK_SIZE - 1) / BLOCK_SIZE,
(n + BLOCK_SIZE - 1) / BLOCK_SIZE);
// TODO: 创建CUDA事件用于计时
// TODO: 启动并计时三个内核
// TODO: 验证结果
// TODO: 输出性能对比
// 释放内存
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
free(h_output);
return 0;
}
扩展要求:
- 尝试不同的块大小(8×8, 16×16, 32×32)并比较性能
- 使用
nvprof或NVIDIA Nsight Compute分析内存访问模式 - 如果矩阵大小不是块大小的整数倍,处理边界情况
题目2:步长访问性能实验
编写一个CUDA程序,研究不同步长对内存访问性能的影响,验证合并访问的理论。
要求:
- 实现一个内核,以不同的步长(stride)访问数组
- 测试步长从1到32(以2的幂次递增)
- 测量每种步长下的内存带宽
- 绘制步长与带宽的关系图
代码框架:
cpp
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
// 以指定步长访问数组的内核
__global__ void strideKernel(float* data, int N, int stride) {
int idx = (blockIdx.x * blockDim.x + threadIdx.x) * stride;
if (idx < N) {
// 执行多次访问以增加可测量性
for (int i = 0; i < 10; i++) {
data[idx] += 1.0f;
}
}
}
// 测量带宽函数
float measureBandwidth(int stride, int N, int blockSize, int repeats) {
float *d_data;
cudaMalloc(&d_data, N * sizeof(float));
cudaMemset(d_data, 0, N * sizeof(float));
// TODO: 创建CUDA事件
int threadsPerBlock = blockSize;
int blocksPerGrid = (N / stride + threadsPerBlock - 1) / threadsPerBlock;
// TODO: 开始计时
for (int i = 0; i < repeats; i++) {
strideKernel<<<blocksPerGrid, threadsPerBlock>>>(d_data, N, stride);
}
// TODO: 结束计时,计算时间
// 计算带宽
// 总访问字节数 = repeats * N * sizeof(float)
// 时间(秒)
// 带宽 = 总字节数 / 时间 (GB/s)
cudaFree(d_data);
return bandwidth;
}
int main() {
int N = 32 * 1024 * 1024; // 32M个float
int blockSize = 256;
int repeats = 10;
printf("步长\t理论利用率\t带宽(GB/s)\n");
// 测试不同步长
for (int stride = 1; stride <= 32; stride *= 2) {
// 计算理论利用率
float theoretical = (stride == 1) ? 1.0f : (1.0f / stride);
// 测量实际带宽
float bandwidth = measureBandwidth(stride, N, blockSize, repeats);
printf("%d\t%.2f%%\t\t%.2f\n", stride, theoretical * 100, bandwidth);
}
return 0;
}
分析要求:
- 解释为什么步长增加会导致带宽下降
- 分析实际带宽与理论利用率的关系
- 讨论在什么步长下性能开始急剧下降
参考答案概要
选择题答案
- B(32字节)
- C(32线程 × 4字节 = 128字节)
- C(128字节 ÷ 32字节/事务 = 4事务)
- C(128/1024 = 12.5%)
- C(关键是从相同32字节段访问)
- C(读操作完全合并)
- B(步长为ld,未合并)
- D(完美合并,利用率100%)
- A(利用率12.5%)
- A(x维度变化最快)
- D(内存事务大小固定为32字节)
- B(连续线程访问连续元素)
- D(ld=1024列,每行1024个float,间隔4096字节)
- B(最大化已用/传输比例)
- C(连续线程访问连续地址)
填空题答案
- 32
- 32
- 128
- 4
- 32,12.5%
- 32字节
- x
- 是,否
- 已用字节数/传输字节数
- 连续
- 32
- 第一个
- 32
- 共享
- 最
简答题要点(示例)
1. 合并内存访问定义和重要性
- 定义:warp内线程的内存请求被合并为最少的内存事务
- 重要性:减少内存事务数量,提高内存带宽利用率,直接影响内核性能
2. warp内存请求合并过程
- 32线程各请求4字节 → 总需求128字节
- GPU检查地址分布 → 合并为4个32字节事务
- 每个事务服务连续8个线程
3. 完美vs未合并对比
- 完美:4事务,128字节传输,利用率100%
- 未合并:32事务,1024字节传输,利用率12.5%
- 差异原因:未合并访问导致大量冗余数据传输
4. 矩阵转置访问分析
- 读合并:myCol变化最快,连续线程读连续列
- 写未合并:myCol作为行索引,步长为ld
- 当ld>32时,每个线程写不同行,地址间隔大
分析题要点(示例)
1. kernel1分析
- 访问地址:0, 8, 16, ...(步长8字节)
- 每个32字节段覆盖4个线程(32/8=4)
- 需要8个事务服务32线程,利用率50%
2. kernel2D分析
- 行优先访问:连续线程访问连续列
- 完全合并访问
- 列优先会破坏合并,因为连续线程访问不同行
3. 各模式利用率
- 模式A:步长1 → 100%
- 模式B:步长2 → 50%
- 模式C:步长32 → 12.5%
- 模式D:步长1(全局索引) → 100%
4. 矩阵转置变体
- 变体B写操作变为合并
- 代价:读操作变为未合并
- 选择取决于后续操作需求
编程题核心实现(示例)
题目1:共享内存转置核心代码
cpp
__global__ void transpose_shared(float* input, float* output, int n) {
__shared__ float tile[BLOCK_SIZE][BLOCK_SIZE];
int x = blockIdx.x * BLOCK_SIZE + threadIdx.x;
int y = blockIdx.y * BLOCK_SIZE + threadIdx.y;
if (x < n && y < n) {
tile[threadIdx.y][threadIdx.x] = input[y * n + x];
}
__syncthreads();
x = blockIdx.y * BLOCK_SIZE + threadIdx.x;
y = blockIdx.x * BLOCK_SIZE + threadIdx.y;
if (x < n && y < n) {
output[y * n + x] = tile[threadIdx.x][threadIdx.y];
}
}
题目2:步长实验预期结果
- 步长1:带宽最高,利用率100%
- 步长2:带宽约下降50%
- 步长4:带宽约下降75%
- 步长8及以上:带宽稳定在低水平
- 实际带宽受硬件限制,不会完全按理论比例下降