想象你是一家火锅连锁店的老板,后厨里雇了一万名切配工。他们技术未必顶级,但手脚麻快、任劳任怨——削土豆、切肉片、摆盘,交给他们又快又好。可如果今天你想让后厨顺便算一算营业流水,工人们只能傻站着:他们只会切菜,不会算账。Compute Shader 就是给这一万工人发的“新岗位培训手册”——不换人、不加薪,只改一张调度卡,就能让同一批帮厨去干“算账”“统计”“配比”这些通用计算活。 在图形学世界里,GPU 就是这家后厨。过去它只被用来“切菜”(渲染三角形),而 Compute Shader 打开了“万能模式”:无论是物理模拟、粒子特效、图像后处理还是大数据排序,只要能把任务拆成成千上万个独立小块,GPU 就能以远超 CPU 的速度并行处理。当然,管好万人工厂也需要学问:怎么给工人分组(Dispatch)、每个小组如何共享工具(共享内存)、下班打卡如何不丢数据(原子操作),都是我们这篇文章要讲的“工厂管理手册”。

岗位关联

掌握 Compute Shader,意味着你能把 GPU 从“画画的机器”变成“万能的并行计算工厂”,这是图形工程师升级为 GPGPU 计算工程师的门票。

目录
  1. 可视化演示
  2. GPU 并行工厂
  3. 计算管线流程
  4. Dispatch 调度
  5. 共享内存
  6. 原子操作
  7. CUDA 与 OpenCL
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Compute Shader的Dispatch线程组调度、共享内存与同步机制、以及与CUDA/OpenCL的多维度对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

'+_captions.dispatch+'

GPU 并行工厂

想象一家火锅店的后厨。CPU 是那位掌勺的大厨,什么菜都会做,动作麻利,但一次只能同时炒一锅。GPU 则是后厨里的“切配工大军”——上千个帮厨各自拿着菜刀,只做“切丝”“切片”这类简单动作,但所有人一起动手,一分钟就能切完整筐土豆。这台“切配机器”单个工人能力有限,但总数惊人。

计算机里的中央处理器(CPU)和图形处理器(GPU)正是这样的分工。CPU 用几个少而精的强核心处理复杂逻辑,追求低延迟;GPU 用几千个简单核心做大规模重复运算,追求高吞吐。Compute Shader 的核心思想,就是把 GPU 这支配菜大军从“只做图形渲染”中解放出来,去处理物理模拟、数据计算、图像处理等任何“可以拆成大量独立小块”的工作,这正是 GPGPU(General-Purpose GPU)通用计算的含义。

GPU 内部采用 SIMT(Single Instruction, Multiple Threads)执行模型:数百个线程按同一套指令并行干活,各自处理不同数据。用 Amdahl 定律可以衡量并行化的收益——任务中可并行比例越高、并行单元越多,加速越明显:

所以,GPU 的优势不取决于“单个厨师多快”,而取决于“像切土豆这样的活儿有多少”。Compute Shader 的职责,就是把任务切成均匀的“土豆丝”,让上万帮厨都能派上用场;而那道“必须由大厨亲手做”的串行步骤——比如决定下一道菜做什么——依然留给 CPU。

S=1(1−p)+pN\displaystyle S = \frac{1}{(1-p) + \frac{p}{N}}
这条公式告诉你:如果任务里 80% 能并行、用 100 个核心,最多加速约 4.8 倍;剩下那 20% 串行部分就是“天花板”。
  1. S 加速比衡量并行化后比原来快了多少倍。S = 4 意味着原来做 4 小时的任务现在只需 1 小时。
    SS
  2. p 并行占比任务中可以拆成一片片同时处理的比例。p = 0.8 表示八成工作可以并行。
    pp
  3. 1-p 串行比例无论多少线程都无法并行的部分,这是并行加速的“天花板”。
    1−p1-p
  4. N 并行规模核心数或线程数,相当于后厨帮厨人数。N 越大分母越小,但收益被串行部分拖累。
    NN

计算管线流程

回到火锅店。后厨原本有一条“备菜流水线”:洗菜→切段→摆盘→上桌,环节固定。可如果客人点的是特制蘸水,这条流水线就不够用了——需要额外几个帮厨在小料台上按配方调酱料。Compute Shader 就像这个“小料台”:它与固定流水线并行存在,专门处理标准流水线覆盖不了的灵活任务。

在图形 API 中,常规渲染依赖“渲染管线”:输入顶点、顶点着色器、像素着色器,一环扣一环。而 Compute Shader 不在这些固定阶段中,它在独立的“计算管线”里运行。调用方用 Dispatch 指令告诉 GPU“开始计算”,GPU 会调度相应数量的线程去执行同一个 Compute Shader 程序。

计算管线的调度需要两个核心参数:每个线程组包含多少线程(S,在 HLSL 里用 numthreads(x,y,z) 声明),以及本次调度启动多少个线程组(G,由 Dispatch(a,b,c) 指定)。总线程数 T 恒等于两者的乘积:

计算管线没有顶点输入语义、没有光栅化步骤,它只关心“给我多少线程、线程各自算哪块数据”。这种简洁的自由度,让 Compute Shader 能灵活介入渲染前、渲染中或渲染后的任何环节——比如先算好布料物理,再把结果交给顶点着色器去渲染。

T=G×S\displaystyle T = G \times S
这句话翻译成大白话是:老板说“准备 12 桌蘸水,每桌 4 个小碟”,那总共就是 48 碟。
  1. T 线程总数本次调度中同时执行的全部线程数量,也就是后厨的总工人数。
    TT
  2. G 线程组数Dispatch 调用的三个维度参数,表示任务被分成几大包。
    GG
  3. S 每组线程数numthreads 声明的组内线程数量,表示每个包里的工人数。
    SS
  4. × 相乘关系总人力 = 包的个数 × 每个包的人数,一包包工人相加。
    ×\times

Dispatch 调度

“3 号包厢 7 号座”的客人想知道自己在餐厅的准确位置;同样,每个切配帮厨在庞大的后厨里也必须知道“我是谁、该切哪筐土豆”。Compute Shader 为每个线程准备了一套坐标系统,让所有人都能算出自己的全局编号。

线程组织分三层:Grid(整个任务网格)→ Group(线程组)→ Thread(组内单个线程)。调用 Dispatch(a,b,c) 时,a×b×c 就是 Group 的数量;每个 Group 内部再按 numthreads(x,y,z) 细分出 Thread。GPU 以 Group 为单位,把线程分发给各个计算单元(SM)去执行。

每个线程在着色器里可以拿到三个 ID:SV_GroupID(组编号)、SV_GroupThreadID(组内编号)、SV_DispatchThreadID(全局编号)。一维情况下,它们满足一个简单换算:

第二、第三维度的索引则按“x + 宽度×y + 宽度×高度×z”的方式展开,本质是同一条公式的多次嵌套。这套坐标系统让 GPU 能把海量线程精准地引导到各自负责的数据切片上——每个线程拿到自己的编号后,就能算出去读全局数组的第几个元素。

i=g×s+t\displaystyle i = g \times s + t
记住“包厢 + 座位”的定位法:客人记住自己是“3 号包厢 7 号座”,前台却能算出他是全局第 127 位客人。
  1. g 组编号SV_GroupID,即当前线程属于第几个 Group,相当于包厢号。
    gg
  2. s 组大小Group 内线程总数,即 numthreads 三轴乘积,相当于每包厢座位数。
    ss
  3. t 组内编号SV_GroupThreadID,线程在 Group 内的局部坐标,相当于座位号。
    tt
  4. i 全局编号SV_DispatchThreadID,整个 Grid 中独一无二的线程编号。
    ii

共享内存

后厨里每个班组共用一个“调料台”:盐、糖、醋都摆在台上,组内任何帮厨伸手就能取用,不必跑回仓库。跨组的帮厨不能乱动对方调料台的东西。这个调料台,对应 GPU 的共享内存(Shared Memory)。

每个线程都频繁访问同一块全局内存时,显存带宽很快会被榨干。共享内存是每个线程组私有的高速缓存区,访问速度比全局内存快一个数量级,容量却很小(通常几十 KB),而且只对组内线程可见。它的价值在于“数据复用”:假设一块数据要被组内 S 个线程反复读取,把它先搬进共享内存,省下的全局带宽为:

不过,共享内存有严格的同步纪律:线程写完共享内存后,必须调用 GroupMemoryBarrierWithGroupSync() 等待组内同伴都读完,否则同伴可能读到旧数据。这就像你刚把盐罐放上调料台,得喊一声“放好了!”,旁边的帮厨才伸手去拿。

Bsaved=(S−1)×b×G\displaystyle B_{\text{saved}} = (S - 1) \times b \times G
共享内存是「组内小卖部」:100 个人都想喝同一桶水,与其每人跑一趟超市(全局内存),不如把水桶搬进小卖部,大家轮流接水。
  1. B_saved 省下的带宽相比“每个线程各自去全局内存读”少传输的字节总量。
    BsavedB_{\text{saved}}
  2. S 组内线程数同一份数据被 S 个线程共享,本来需要重复读 S 次。
    SS
  3. b 单块数据字节数每个被复用数据块的大小。
    bb
  4. G 组数每个线程组都这样复用,收益再放大 G 倍。
    GG

原子操作

晚高峰的后厨门口只有一台打卡机。帮厨们下班时挤着打卡,如果大家同时把手指按上去,打卡机就懵了:要么只记下一个人,要么把指纹混在一起。要保证每个人的出勤都记录在案,就得让打卡机“一次只服务一个人”——这就是原子操作。

GPU 线程天然并发。多个线程同时执行 sum = sum + 1 时,这一行其实被拆成“读旧值→加 1→写回”三步。若两个线程同时读到旧值 0,都会写回 1,结果便少了 1。原子操作(如 InterlockedAdd)保证这三步在硬件层面不可分割。

用原子操作把计数器从初始值叠加 M 次(M 为发起原子加的线程数),最终结果一定等于:

原子操作保证了正确性,但代价是“排队”。几百个线程挤在同一个全局变量上“打卡”,竞争会严重拖慢速度。工程上更优的做法是:每个线程组先用共享内存做局部归约,每组算出一个“小计”,再由每组的一个线程对全局做一次原子加——把千军万马过独木桥,改成各班组先内部汇总、再派代表上交。

Sfinal=Sinit+M\displaystyle S_{\text{final}} = S_{\text{init}} + M
这一行是原子操作的“数学承诺”:排队打卡,一个都不能少。
  1. S_final 最终值所有并发原子加完成后的全局计数结果。
    SfinalS_{\text{final}}
  2. S_init 初始值计数器在本次调度开始前的取值。
    SinitS_{\text{init}}
  3. M 原子加次数有多少个线程执行过 InterlockedAdd。
    MM
  4. 等号保证若没有原子保护,并发写会互相覆盖,最终值会小于 init+M;原子操作让等号严格成立。
    ==

CUDA 与 OpenCL

市面上有好几个“火锅店连锁集团”:微软的 Direct3D、NVIDIA 的 CUDA、还有跨平台的开放标准 OpenCL。它们后厨里其实是同一批帮厨机器,只是工牌叫法不同:有的叫“桌号”,有的叫“包厢号”,有的叫“区位号”,干的活完全一致。

Compute Shader 是 Direct3D 体系里的叫法;NVIDIA 的 CUDA 使用 kernel 与 Thread/Block/Grid 概念;Khronos 的 OpenCL 则使用 Work-item/Work-group/NDRange。三者底层都是同一个 GPGPU 模型,只是 API 生态、硬件绑定和支持范围不同。

三个方案的术语映射一目了然:

所以,学会 Compute Shader 后,切换到 CUDA 或 OpenCL 并不会从零开始——你只是换了套工牌。真正值钱的核心能力,是理解“如何让并行大军分班协作、共享调料台、排队打卡”,这三件事在所有 GPGPU 方案里都是相通的。

(ThreadGroupGrid)HLSL⇔(ThreadBlockGrid)CUDA⇔(Work-itemWork-groupNDRange)OpenCL\displaystyle \begin{pmatrix} \text{Thread} \\ \text{Group} \\ \text{Grid} \end{pmatrix}_{\text{HLSL}} \Leftrightarrow \begin{pmatrix} \text{Thread} \\ \text{Block} \\ \text{Grid} \end{pmatrix}_{\text{CUDA}} \Leftrightarrow \begin{pmatrix} \text{Work-item} \\ \text{Work-group} \\ \text{NDRange} \end{pmatrix}_{\text{OpenCL}}
把“包厢”改叫"Block“,把”桌号“改叫”Work-group",搬家不换菜谱。
  1. Thread / Thread / Work-item 同一层三者都是最细粒度的并行执行单元,即单个帮厨。
    Thread⇔Work-item\text{Thread} \Leftrightarrow \text{Work-item}
  2. Group / Block / Work-group 同一层可共享内存的最小线程集合,即一个班组。
    Group⇔Block\text{Group} \Leftrightarrow \text{Block}
  3. Grid / Grid / NDRange 同一层整个任务覆盖的全部线程集合,即整个后厨。
    Grid⇔NDRange\text{Grid} \Leftrightarrow \text{NDRange}
  4. 生态差异与选择图形管线内选 HLSL Compute Shader;NVIDIA 科学计算生态(cuDNN、TensorRT)选 CUDA;跨厂商嵌入式平台选 OpenCL。
    Graphics→HLSL,Science→CUDA,Open→OpenCL\text{Graphics} \to \text{HLSL}, \quad \text{Science} \to \text{CUDA}, \quad \text{Open} \to \text{OpenCL}

AI 应用场景

游戏物理引擎中的粒子系统

阶段:实时渲染与模拟加速

解决的问题:一个爆炸场景里可能有上万颗粒子,CPU 串行更新每颗粒子的受力、速度和位置,帧率会掉到个位数。用 Compute Shader 把每颗粒子的更新交给一个 GPU 线程,一万颗粒子瞬间就能并行算完。

具体案例:虚幻引擎的 Niagara 粒子系统就是典型——用 Compute Shader 在 GPU 上并行模拟粒子运动,再把结果 Buffer 交给渲染管线绘制,实现百万粒子级别的烟火、碎片和流体特效。

实时图像后处理

阶段:渲染管线的后处理阶段

解决的问题:Bloom(泛光)、高斯模糊、色调映射等逐像素操作,在 CPU 上处理 1080p 画面每帧要遍历 200 万个像素,性能完全不可接受。这些滤镜高度并行,正是 Compute Shader 的主场。

具体案例:游戏引擎的 Bloom 后处理链中,先用 Compute Shader 对高亮区域降采样、做多次模糊,再叠加回原图。整个过程都跑在 GPU 计算管线上,不打断渲染流程,帧时间只增加 1-2 毫秒。

GPU 加速的大规模数据归约与排序

阶段:GPGPU 通用计算阶段

解决的问题:百万级数组的求和、直方图统计、基数排序,在 CPU 上是分钟级任务。利用 Compute Shader 的分组归约 + 原子操作,可以在毫秒级完成。

具体案例:数据库系统用 GPGPU 做聚合查询加速:每个线程处理一批数据,组内共享内存做局部归约,再由每组的代表线程用原子操作把结果合并到全局——这正是本文讲的“先小计、再上报”策略的工程落地。

Python 演示

python52 行
import threadingimport time GROUP_SIZE = 32NUM_GROUPS = 4NUM_THREADS = GROUP_SIZE * NUM_GROUPS  def global_index(group_id, local_id):    """模拟 GPU 全局索引换算:gid = group_id * GROUP_SIZE + local_id"""    return group_id * GROUP_SIZE + local_id  def run_experiment(use_atomic):    shared = [0]    lock = threading.Lock()     def work(group_id, local_id):        gid = global_index(group_id, local_id)        if use_atomic:            with lock:  # 模拟原子操作:读-改-写不可分割                old = shared[0]                time.sleep(0.0005)  # 模拟内存访问延迟                shared[0] = old + 1        else:            old = shared[0]            time.sleep(0.0005)            shared[0] = old + 1     threads = []    for g in range(NUM_GROUPS):        for l in range(GROUP_SIZE):            threads.append(threading.Thread(target=work, args=(g, l)))    for t in threads:        t.start()    for t in threads:        t.join()    return shared[0]  def main():    expected = NUM_THREADS    unsafe_result = run_experiment(use_atomic=False)    atomic_result = run_experiment(use_atomic=True)    print("期望总和       :", expected)    print("无原子保护结果 :", unsafe_result, " 丢失", expected - unsafe_result, "次更新")    print("原子保护结果   :", atomic_result, " 结果正确")    assert atomic_result == expected  if __name__ == "__main__":    main()
目的:用 Python 线程模拟 Compute Shader 的分组线程并发求和,直观展示“无原子保护会丢更新、有原子保护结果正确”。
  1. shared = [0]用单元素列表充当全局内存中的计数器。
  2. global_index(group_id, local_id)对应公式 i = g × s + t:组编号 × 组大小 + 组内编号,算出全局线程编号。
  3. old = shared[0]模拟 GPU 上的“读”操作:线程把旧值取到本地。
  4. time.sleep(0.0005)人为制造读与写之间的延迟窗口,让多个线程同时读到旧值,加剧竞争。
  5. with lock:模拟 InterlockedAdd 的原子保护:读-改-写三步不可分割,排队执行。
  6. threads.append(threading.Thread(...))创建 128 个线程,对应 GPU 的 128 个并行线程。
  7. assert atomic_result == expected原子保护下的结果必须等于 128,这是原子操作的正确性保证。

常见误区

误区Compute Shader 名字里带 "Shader",那它肯定只服务于游戏渲染,做 Graphics 的人才需要学。

正解Compute Shader 是 GPGPU 通用计算工具,物理模拟、数据处理、科学计算、AI 推理都能用。它叫 Shader 只是因为出身在图形 API 里,能力完全不限于图形。

为何常见:它诞生于图形 API、教程大多以渲染效果为例,给初学者留下了“图形专用”的第一印象。

误区既然 GPU 并行能力强,那把 `numthreads(1024,1,1)` 直接拉满,性能肯定最好。

正解GPU 每个 SM 能驻留的线程数和资源(寄存器、共享内存)都有限,线程过多会导致资源不足、调度开销变大、占用率反而下降。最佳线程数需要结合硬件规格实测。

为何常见:“线程越多并行度越高”的直觉被过度推广,忽略了硬件资源的硬约束。

误区共享内存比全局内存快,那就往 `groupshared` 数组里塞尽量多的数据,一定能加速。

正解共享内存容量极小(通常 32-48KB),占用过多会限制 SM 驻留的线程组数量,最终拉低占用率和并行度。空间换速度要算总账。

为何常见:只看到了共享内存“快”的一面,忽略了它“小”和“独占”的一面。

误区反正 InterlockedAdd 能保证正确,那所有累加、计数都直接对全局变量用原子操作就好了。

正解大量线程争抢同一个原子变量会串行化,竞争激烈时性能断崖式下跌。工程上应先用共享内存在组内归约,再让每组一个线程做全局原子加。

为何常见:原子操作调用简单、语义安全,容易让人忽略背后的硬件仲裁和排队开销。

误区把 CPU 多线程开发的经验搬到 GPU 上——线程创建要小心、数量不宜太多、要避免频繁切换。

正解GPU 线程极轻量,硬件调度器以 warp/wavefront 为单位批量切换,创建成千上万个线程是常态。CPU 线程是重量级资源,GPU 线程是流水线上的工件。

为何常见:两者都叫“线程”,误导开发者用同一套心智模型去理解完全不同的硬件。

面试题

Q1. Compute Shader 最主要解决什么问题?

单选题 · easy

  • A.提高 CPU 的渲染速度和效率
  • B.让 GPU 执行通用并行计算任务← 正确答案
  • C.简化着色器编译流程和调试
  • D.增加显存容量和访问带宽

答案:B。Compute Shader 的本质是 GPGPU,把 GPU 从图形渲染扩展到通用计算。它不是为了给 CPU 提速,也不负责编译优化和显存扩容,而是让 GPU 去做物理模拟、数据处理等通用并行任务。

Q2. 在 HLSL 中,numthreads(8,4,2) 声明的是什么?

单选题 · easy

  • A.Dispatch 启动的线程组三维数量
  • B.共享内存分配的字节大小
  • C.每组内部的线程三维布局← 正确答案
  • D.整个 Grid 的线程总数量

答案:C。numthreads 声明的是“每个 Group 里有 8×4×2 个线程”,是组内的三维布局,对应公式 T = G × S 中的 S。Dispatch 参数才是组数 G,共享内存大小则用 groupshared 声明。

Q3. 调用 Dispatch(6,4,1) 且 numthreads(8,4,1),总线程数是多少?

单选题 · mid

  • A.24,仅把组数三个维度相乘
  • B.32,仅把组内三个维度相乘
  • C.768,组数乘每组线程数← 正确答案
  • D.96,维度交叉相乘

答案:C。总线程数 T = G × S = (6×4) × (8×4) = 24 × 32 = 768。A 只算了组数、B 只算了组内线程、D 的交叉相乘没有物理意义。记住:先算多少组,再算每组多少人,两者相乘才是总人力。

Q4. 下列哪些关于共享内存的说法是正确的?

多选题 · mid

  • A.组内所有线程都能访问它← 正确答案
  • B.不同线程组的线程可以互相访问
  • C.访问延迟远低于全局内存← 正确答案
  • D.无需同步就能保证写入可见

答案:AC。共享内存是组内私有高速缓存,类似“组内调料台”,只对同组线程可见(A 对、B 错)。它比全局内存快一个数量级(C 对)。但线程间存在竞争,必须用 GroupMemoryBarrierWithGroupSync 同步才能保证写入对其他线程可见(D 错)。

Q5. 在 HLSL Compute Shader 中,哪个函数用于组内线程同步?

单选题 · mid

  • A.组内共享内存屏障同步← 正确答案
  • B.全局内存写入屏障同步
  • C.顶点着色器阶段跳转函数
  • D.像素颜色混合输出函数

答案:A。GroupMemoryBarrierWithGroupSync() 的作用是“等我写好了共享内存,组内所有人都读完再继续”,对应选项 A 的描述。B 对应的是设备级屏障,C、D 与同步无关。没有这道屏障,组内线程可能读到旧数据。

Q6. InterlockedAdd(sharedVar, 1) 的语义保证是什么?

单选题 · hard

  • A.各线程按随机顺序执行且结果有丢失
  • B.读改写三步不可分割且结果不丢失← 正确答案
  • C.所有并发加操作合并为一次写入
  • D.只有优先级最高的线程能成功执行

答案:B。原子操作保证“读旧值→加 1→写回”在硬件层面不可分割,M 次原子加的结果严格等于初始值 + M。它并不保证按某种顺序,也不合并写入,更不是只让一个线程成功。好比打卡机一次服务一人,但所有人都会被记录。

Q7. 以下哪些策略能减少原子归约的竞争开销?

多选题 · hard

  • A.所有线程直接对全局变量原子加
  • B.组内先归约再每组一次全局原子加← 正确答案
  • C.用普通写替代原子加再读回验证
  • D.每线程写独立地址最后串行合并← 正确答案

答案:BD。A 会让成百上千线程挤在同一个变量上排队,性能差;C 用普通写替代原子加会丢更新。B(共享内存局部归约 + 每组一次原子加)和 D(线程私有地址 + 最后合并)都减少了全局原子操作的争抢次数,是工程上推荐的归约策略。

Q8. CUDA 中的 Block 对应 HLSL Compute Shader 的哪个概念?

单选题 · easy

  • A.Group← 正确答案
  • B.Thread
  • C.Grid
  • D.Pixel

答案:A。CUDA 的层级是 Thread/Block/Grid,HLSL 计算着色器是 Thread/Group/Grid。Block 与 Group 同级,都是“可以共享内存的最小线程集合”,OpenCL 里则叫 Work-group。换平台只是换工牌,模型本质相同。

Q9. Compute Shader 与像素着色器相比,哪些说法是正确的?

多选题 · mid

  • A.不在固定渲染管线阶段中执行← 正确答案
  • B.可读写 UAV 等通用计算资源← 正确答案
  • C.没有顶点输入语义的绑定← 正确答案
  • D.只能输出颜色到渲染目标

答案:ABC。Compute Shader 运行在独立计算管线,不经过光栅化等固定阶段(A 对);它可以自由读写 UAV、StructuredBuffer 等通用资源(B 对);它没有语义绑定的顶点输入(C 对)。它并不只能输出颜色,而是可以输出任意计算结果到缓冲区(D 错)。

Q10. GPU 一个 SM 有 32KB 共享内存,每个线程组需要 16KB,SM 最多驻留 8 组,实际最多驻留几组?

单选题 · extreme

  • A.无限制,只取决于调度器调度
  • B.8 组,受 SM 驻留上限约束
  • C.2 组,受共享内存容量限制← 正确答案
  • D.4 组,受线程数量限制

答案:C。共享内存 32KB ÷ 每组 16KB = 2 组,这是共享内存维度给出的硬上限;驻留上限 8 组虽高,但实际取两者较小值 min(8, 2) = 2 组。这类题目考查“资源瓶颈取最小”的思想,也是实际调优中排查占用率上不去的常用方法。