以下是针对 2.2.3 GPU Device Memory Spaces 章节内容设计的完整知识点巩固习题包,包含选择题、填空题、简答题、分析题和编程练习题。
GPU设备内存空间知识巩固习题包
一、选择题(每题只有一个正确答案)
1. 以下哪种内存类型具有网格级别的作用域和应用程序级别的生命周期?
A. 共享内存
B. 寄存器
C. 全局内存
D. 局部内存
2. 共享内存的物理位置在哪里?
A. 设备内存(DRAM)
B. 流多处理器(SM)内部
C. CPU主存
D. L2缓存
3. 关于寄存器的下列说法,正确的是?
A. 寄存器由程序员显式管理
B. 寄存器的作用域是整个网格
C. 寄存器是速度最快的存储类型
D. 寄存器的容量通常比全局内存大
4. 以下哪个API用于获取GPU的设备属性,包括共享内存大小和寄存器数量?
A. cudaGetDeviceCount()
B. cudaGetDeviceProperties()
C. cudaDeviceGetAttribute()
D. cudaMemGetInfo()
5. 局部内存的物理位置在哪里?
A. SM内部的专用存储
B. L1缓存中
C. 全局内存中
D. 寄存器文件中
6. 以下哪种情况最可能导致编译器将变量放在局部内存中?
A. 变量是int类型
B. 变量是float类型
C. 使用非常大的结构体或数组
D. 变量在循环中使用
7. 关于共享内存和L1缓存的关系,下列说法正确的是?
A. 它们是完全独立的不同硬件资源
B. 它们共享同一物理资源,使用共享内存会减少L1缓存可用空间
C. 共享内存是L1缓存的一部分
D. 它们互不影响
8. 在动态分配共享内存时,如果需要多个不同类型的数组,应该怎么做?
A. 多次声明extern __shared__
B. 使用单个extern __shared__指针并手动分区,注意对齐
C. 使用静态分配代替
D. 分别使用cudaMalloc分配
9. __syncthreads()的作用是什么?
A. 同步网格中的所有线程
B. 同步设备上的所有线程
C. 同步同一线程块内的所有线程
D. 同步主机和设备
10. 以下哪个函数用于设置内核的缓存偏好(偏向共享内存或L1缓存)?
A. cudaSetCacheConfig()
B. cudaDeviceSetCacheConfig()
C. cudaFuncSetCacheConfig()
D. cudaKernelSetCacheConfig()
11. 关于常量内存,下列说法错误的是?
A. 作用域是整个网格
B. 生命周期是整个应用程序
C. 可以随时修改
D. 具有缓存优化
12. 在vecAdd内核示例中,数组A、B、C存储在哪种内存中?
A. 共享内存
B. 寄存器
C. 全局内存
D. 常量内存
13. 如果内核使用的寄存器超过可用数量,会发生什么?
A. 内核启动失败
B. 编译器报错
C. 发生寄存器溢出,变量被移到局部内存
D. 自动使用共享内存
14. 关于共享内存的静态分配,下列说法正确的是?
A. 大小可以在运行时确定
B. 使用extern __shared__声明
C. 使用__shared__声明,大小在编译时确定
D. 只能在主机端分配
15. 以下哪项不是全局内存的特点?
A. 所有线程可访问
B. 生命周期为整个应用程序
C. 位于SM内部
D. 通过cudaMalloc分配
二、填空题
1. CUDA设备有几种主要的内存类型: 、 、 、 ** 和 _____。
**2. 全局内存的分配使用 _____ 函数,释放使用 _____ 函数。
**3. 共享内存的作用域是 _____ ,生命周期是 _____。
**4. 寄存器的作用域是 _____ ,生命周期是 _____。
**5. 局部内存虽然逻辑上是线程局部存储,但物理上位于 _____ 中。
**6. 同步同一线程块内所有线程的函数是 _____。
**7. 动态分配共享内存在内核启动时通过 <<<grid, block, **_____**>>> 指定大小。
**8. 在内核中动态分配共享内存需要使用 _____ 说明符声明。
**9. 使用 cudaFuncSetCacheConfig 可以设置内核的 _____ 偏好。
**10. 编译器选项 _____ 可以限制内核使用的最大寄存器数量。
**11. 查询设备共享内存大小可以使用 cudaDeviceProp 结构体的 _____ 和 _____ 成员。
**12. 当数组索引不是编译时常量时,编译器可能会将数组放在 _____ 中。
**13. 寄存器溢出会导致变量被存储在 _____ 中,性能 _____。
**14. 共享内存与 _____ 缓存共享同一物理资源。
**15. 连续线程访问连续32位字时,局部内存访问可以实现 _____。
三、简答题
1. 比较全局内存和共享内存的特点,说明各自适用的场景。
2. 解释为什么需要使用__syncthreads(),并说明使用时的注意事项。
3. 描述寄存器溢出的原因、影响以及如何避免。
4. 说明局部内存的物理位置和逻辑作用域,以及什么情况下变量会被放入局部内存。
5. 比较静态分配和动态分配共享内存的方法,并说明各自的适用场景。
6. 在动态分配多个共享内存数组时,为什么要考虑对齐问题?给出一个正确分区的示例。
7. 解释cudaFuncSetCacheConfig的作用及其局限性。
8. 为什么内核计算结果必须通过全局内存返回给主机?
9. 说明线程块内多个线程同时访问共享内存时可能遇到的问题及解决方案。
10. 比较寄存器、共享内存和全局内存在速度、容量和作用域方面的差异。
四、分析题
1. 分析以下代码,指出其中可能存在的内存相关问题,并提出改进建议:
cpp
__global__ void processData(float* input, float* output, int N) {
__shared__ float cache[256];
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 从全局内存加载数据到共享内存
if (idx < N) {
cache[threadIdx.x] = input[idx];
}
// 对共享内存中的数据进行处理
float sum = 0.0f;
for (int i = 0; i < blockDim.x; i++) {
sum += cache[i];
}
// 将结果写回全局内存
if (threadIdx.x == 0) {
output[blockIdx.x] = sum;
}
}
2. 分析以下动态共享内存分配代码,找出其中的错误并说明原因:
cpp
__global__ void dynamicSharedKernel(float* data) {
extern __shared__ float shared[];
// 想要分配两个数组:short类型(128个)和float类型(64个)
short* array0 = (short*)shared;
float* array1 = (float*)&array0[127]; // 使用127个short
// 使用数组
if (threadIdx.x < 128) {
array0[threadIdx.x] = (short)threadIdx.x;
}
__syncthreads();
if (threadIdx.x == 0) {
for (int i = 0; i < 64; i++) {
array1[i] = (float)array0[i * 2]; // 访问array1
}
}
}
// 启动配置
int sharedMemSize = 128 * sizeof(short) + 64 * sizeof(float);
kernel<<<grid, block, sharedMemSize>>>(d_data);
3. 分析以下代码中变量的存储位置:
cpp
__constant__ float coeff[16];
__global__ void analyzeKernel(float* globalData, int N) {
__shared__ int blockCounts[64];
int localIdx = threadIdx.x + blockIdx.x * blockDim.x;
float regVal = 3.14f;
int localArray[4];
for (int i = 0; i < 4; i++) {
localArray[i] = localIdx * i;
}
if (localIdx < N) {
blockCounts[threadIdx.x] = (int)(globalData[localIdx] * coeff[threadIdx.x % 16]);
}
__syncthreads();
if (threadIdx.x == 0) {
int sum = 0;
for (int i = 0; i < blockDim.x; i++) {
sum += blockCounts[i];
}
globalData[blockIdx.x] = (float)sum;
}
}
请分析以下每个变量的存储位置和作用域:
coeffglobalData(指针指向的数据)blockCountslocalIdxregVallocalArrayi(循环变量)
4. 分析以下关于寄存器使用的代码,评估可能存在的性能问题:
cpp
__global__ void registerHeavyKernel(float* input, float* output, int N) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < N) {
float a1 = input[idx];
float a2 = a1 * 2.0f;
float a3 = a2 * 1.5f;
float a4 = a3 + 1.0f;
float a5 = a4 / 2.0f;
float a6 = a5 * a1;
float a7 = a6 - a2;
float a8 = a7 * 1.2f;
float a9 = a8 + a3;
float a10 = a9 * 0.8f;
float a11 = a10 / 1.1f;
float a12 = a11 + a4;
float a13 = a12 * 2.3f;
float a14 = a13 - a5;
float a15 = a14 * 1.7f;
float a16 = a15 + a6;
float a17 = a16 * 0.5f;
float a18 = a17 / 1.3f;
float a19 = a18 + a7;
float a20 = a19 * 2.1f;
output[idx] = a20;
}
}
问题:
- 这个内核可能面临什么问题?
- 如何诊断寄存器使用情况?
- 有什么优化策略?
5. 分析以下合并访问模式的代码:
cpp
__global__ void accessPatternKernel(float* data, int width) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
// 访问模式1
int idx1 = y * width + x;
data[idx1] = 1.0f;
// 访问模式2
int idx2 = x * width + y;
data[idx2] = 2.0f;
}
假设:
- 线程块配置:
dim3 block(32, 8) - 网格配置:
dim3 grid(16, 16) - width = 1024
问题:
- 哪种访问模式可以实现合并访问?为什么?
- 解释两种模式的内存访问方式。
五、编程练习题
题目1:矩阵转置优化(使用共享内存)
编写一个CUDA程序实现矩阵转置,要求使用共享内存来优化内存访问。
要求:
- 实现一个不使用共享内存的简单转置内核
- 实现一个使用共享内存的优化转置内核(考虑内存合并访问)
- 对比两种实现的性能差异
代码框架:
cpp
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
// 简单转置内核(不使用共享内存)
__global__ void transposeSimple(float* input, float* output, int width, int height) {
// TODO: 实现简单转置
}
// 使用共享内存的转置内核(每个线程处理一个元素)
__global__ void transposeShared(float* input, float* output, int width, int height) {
// TODO: 使用共享内存优化转置
// 提示:使用 __shared__ 声明共享内存块
}
// 验证函数
bool verifyResult(float* h_input, float* h_output, int width, int height) {
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (h_output[j * height + i] != h_input[i * width + j]) {
return false;
}
}
}
return true;
}
int main() {
int width = 1024;
int height = 1024;
size_t size = width * height * sizeof(float);
// 分配主机内存
float* h_input = (float*)malloc(size);
float* h_output = (float*)malloc(size);
// 初始化数据
for (int i = 0; i < width * height; 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(16, 16);
dim3 grid((width + block.x - 1) / block.x, (height + block.y - 1) / block.y);
// 启动简单转置内核
transposeSimple<<<grid, block>>>(d_input, d_output, width, height);
cudaDeviceSynchronize();
// 启动共享内存转置内核
cudaMemset(d_output, 0, size);
transposeShared<<<grid, block>>>(d_input, d_output, width, height);
cudaDeviceSynchronize();
// 检查错误
cudaError_t err = cudaGetLastError();
if (err != cudaSuccess) {
printf("Kernel launch error: %s\n", cudaGetErrorString(err));
}
// 拷贝结果回主机
cudaMemcpy(h_output, d_output, size, cudaMemcpyDeviceToHost);
// 验证结果
if (verifyResult(h_input, h_output, width, height)) {
printf("Transpose successful!\n");
} else {
printf("Transpose failed!\n");
}
// 释放内存
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
free(h_output);
return 0;
}
进阶挑战:
- 修改内核使每个线程处理多个元素(例如4x4图块)
- 使用
cudaEvent_t测量两种内核的执行时间 - 处理非正方形矩阵(width != height)的情况
题目2:向量求和归约(使用共享内存)
编写一个CUDA程序,使用共享内存实现高效的向量求和归约操作。
要求:
- 实现一个使用全局内存的简单归约
- 实现一个使用共享内存的优化归约(块内归约)
- 处理任意长度的向量(可能不是块大小的整数倍)
代码框架:
cpp
#include <cuda_runtime.h>
#include <stdio.h>
// 简单归约(使用全局内存)
__global__ void reduceSimple(float* input, float* output, int N) {
// TODO: 实现简单归约
}
// 使用共享内存的优化归约
__global__ void reduceShared(float* input, float* output, int N) {
extern __shared__ float shared[];
// TODO: 实现共享内存归约
// 提示:
// 1. 加载数据到共享内存
// 2. 块内同步
// 3. 执行树形归约
// 4. 将块结果写回全局内存
}
int main() {
int N = 1000000;
size_t size = N * sizeof(float);
// 分配主机内存
float* h_input = (float*)malloc(size);
float h_result = 0.0f;
// 初始化数据
for (int i = 0; i < N; i++) {
h_input[i] = 1.0f; // 方便验证,总和应为N
h_result += h_input[i];
}
// 分配设备内存
float *d_input, *d_output;
cudaMalloc(&d_input, size);
// 计算需要的块数(每个块产生一个部分和)
int blockSize = 256;
int numBlocks = (N + blockSize - 1) / blockSize;
cudaMalloc(&d_output, numBlocks * sizeof(float));
// 拷贝数据到设备
cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
// TODO: 启动归约内核
reduceShared<<<numBlocks, blockSize, blockSize * sizeof(float)>>>(
d_input, d_output, N
);
// TODO: 将部分和从设备拷贝回主机并求和
float* h_partial = (float*)malloc(numBlocks * sizeof(float));
cudaMemcpy(h_partial, d_output, numBlocks * sizeof(float), cudaMemcpyDeviceToHost);
float gpu_result = 0.0f;
for (int i = 0; i < numBlocks; i++) {
gpu_result += h_partial[i];
}
// 验证结果
printf("CPU result: %f\n", h_result);
printf("GPU result: %f\n", gpu_result);
printf("Difference: %f\n", h_result - gpu_result);
// 释放内存
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
free(h_partial);
return 0;
}
进阶挑战:
- 实现归约的最后一步在GPU上完成(不使用CPU)
- 避免线程束发散
- 使用展开技术优化循环
参考答案概要
选择题答案
- C
- B
- C
- B
- C
- C
- B
- B
- C
- C
- C
- C
- C
- C
- C
填空题答案
- 全局内存、共享内存、寄存器、局部内存、常量内存
- cudaMalloc, cudaFree
- 线程块,内核执行期间
- 线程,内核执行期间
- 全局内存
- __syncthreads()
- sharedMemoryBytes
- extern shared
- 缓存/共享内存
- -maxrregcount
- sharedMemPerMultiprocessor, sharedMemPerBlock
- 局部内存
- 局部内存,下降
- L1
- 合并访问
简答题、分析题和编程题答案要点(部分示例)
简答题1要点:
- 全局内存:容量大,延迟高,所有线程可访问,适合存储主要数据
- 共享内存:容量小,延迟低,块内共享,适合频繁访问的数据和线程协作
分析题1答案要点:
- 问题1:缺少__syncthreads()同步,可能导致部分线程未完成数据加载
- 问题2:每个线程读取整个共享数组,效率低
- 改进:在读取cache前添加__syncthreads(),使用树形归约代替串行累加
编程题1核心实现:
cpp
__global__ void transposeShared(float* input, float* output, int width, int height) {
__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 < width && y < height) {
tile[threadIdx.y][threadIdx.x] = input[y * width + x];
}
__syncthreads();
int newX = blockIdx.y * BLOCK_SIZE + threadIdx.x;
int newY = blockIdx.x * BLOCK_SIZE + threadIdx.y;
if (newX < height && newY < width) {
output[newY * height + newX] = tile[threadIdx.x][threadIdx.y];
}
}
以上习题包全面覆盖了 2.2.3 GPU Device Memory Spaces 章节的核心知识点,适合用于自我测试和巩固学习。