想象你是一家爆火餐厅的老板,客人排到了街角,可你的后厨只有一口锅。你不可能亲自去切每一根葱、翻每一片肉——你需要一位身经百战的厨师长。你只需要在菜单上写下:“这十道菜,每道做一万份”,厨师长就会把任务拆给一千个帮厨,让它们在同一时间切配、焯水、翻炒、摆盘,你甚至不用关心每个帮厨手里的刀是怎么挥的。Thrust 就是 CUDA 世界里的这位厨师长:它是 NVIDIA 提供的 GPU 高层算法库,长着一张 C++ 标准库(STL)的脸。你用 sort 排序、用 reduce 求和、用 transform 做逐元素变换,就像调用普通 C++ 函数一样——但背后,成千上万个 GPU 线程已经在“厨房”里热火朝天地干起来了。你从“手写每一个内核(kernel)”的繁琐中解放出来,只需要关心“菜单”(算法逻辑),不用关心“刀工”(线程调度)。这篇文章,就带你认识这位厨师长,学会向他点菜。
Thrust 让 GPU 编程的门槛从“手写内核的精细活”降到“像调用 STL 一样自然”,掌握它,你就能快速产出安全高效的并行代码,是 GPU 计算工程师的必备技能。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
Thrust初识
你刚接管一间大后厨,听老师傅说,以前做菜得自己从磨刀学起——对应着 CUDA 编程里手写 kernel、管理线程块和网格的繁琐步骤。但你真正想做的是“用一万份食材做出同样味道的招牌菜”。这时老厨师告诉你:别慌,我们有一位厨师长叫 Thrust。你只管把菜单递给他,他负责让一千个帮厨在同一秒钟同时开工。
Thrust 是 NVIDIA 在 CUDA 之上封装的高层模板库,头文件放在 <thrust/...> 里。它的接口刻意模仿 C++ 标准库:正如 std::vector 之于 CPU,thrust::device_vector 之于 GPU;正如 std::sort 之于数组,thrust::sort 之于设备内存。你不需要手动分配显存、指定网格尺寸、写 __global__ 函数,只需要描述“要做什么”。
一次 thrust::sort(d_vec.begin(), d_vec.end()) 调用,库内部会自动选择合适的 kernel 并调度所有线程。底层依赖 C++ 模板与 functor/lambda 机制,所以你的代码风格从“指挥每一步”变成了“描述意图”——这正是厨师长存在的意义:菜单上写什么,他就做什么,怎么做是他的事。
1. T_单核(串行耗时):一个厨师按照菜谱从头做到尾的总时间。
2. T_并行(并行耗时):厨师长把菜谱拆给 N 个帮厨后,大家同时开工完成的总时间。
3. N_线程(帮厨人数):GPU 上同时干活的线程数量,通常成千上万。
4. 加速比(快了多少倍):串行时间除以并行时间,理想情况下近似等于线程数 N。
当然,理想的加速比不会真正拿到——帮厨之间要传话、锅要轮流用,这就是后文会遇到的传输开销与调度开销。但至少,你把“指挥一千双手”这件事,完全交给了厨师长。
设备向量
开一家餐厅,第一件事是找地方存食材。CPU 侧的 std::vector 是“一楼前台冰箱”(主机内存);GPU 侧的 thrust::device_vector 是“后厨冷藏柜”(显存)。冷藏柜紧挨着炒锅(处理器),帮厨取料最快——但你别隔三差五把食材从一楼搬到二楼,来回跑楼梯可要命了。
device_vector 的用法几乎和 std::vector 一模一样:thrust::device_vector<int> v(1024); 创建一个能装 1024 个整数的冷藏柜;v.size() 看容量;v[0] 访问单个元素;thrust::fill(v.begin(), v.end(), 7) 把所有格子都填上 7。乍一看毫无 GPU 痕迹,这就是 STL 风格接口的魅力。
真正需要注意的是数据搬运。thrust::host_vector 对应主机内存,thrust::device_vector 对应显存,两者之间用 thrust::copy 同步。每次 CPU↔GPU 拷贝都相当于把食材从一楼搬上二楼再搬下来,PCIe 带宽有限,来回跑非常费时间。所以最佳实践是:数据一次搬进 GPU,连续完成多步算法,最后再搬回一次。
1. T_H2D(主机到设备拷贝):把食材从一楼前台搬进后厨冷藏柜的时间。
2. T_D2H(设备回传主机):把成品从后厨端回一楼前台的时间。
3. T_计算(真正处理的时间):帮厨们实际切菜炒菜所用的时间。
4. 搬运占比(开销比例):如果这个比值接近 1,说明你一直在搬货而不是做菜——这也是 GPU 菜鸟最常见的性能浪费。
记住:设备向量是后厨的冷藏柜。食材住得越久、来回搬的次数越少,菜(程序)就上得越快。让数据“住”在设备端,是玩转 Thrust 的第一条心法。
核心算法
厨师长最拿手的三道主厨招牌菜:变换、归约、排序。你只需要报菜名,他就指挥全体帮厨按分工行动。这三道菜分别对应 thrust::transform、thrust::reduce、thrust::sort,是绝大多数 GPU 任务的“三板斧”。
transform 变换
transform 对每个食材(元素)独立执行同一个操作——就像把一万根胡萝卜同时放进切丝机,每根出来都是丝。它适合做逐元素运算:取绝对值、缩放、归一化、套激活函数。因为每个输出只依赖对应的一个输入,天然并行,GPU 上轻松跑满。
1. x_i(第 i 个输入):第 i 份原始食材,来自输入容器的当前位置。
2. f(处理函数):切丝机,由用户自定义,比如取平方、加偏置、sigmoid 激活。
3. y_i(第 i 个输出):处理之后的食材,与输入一一对应,输出容器同位存放。
4. i 从 0 到 n-1(遍历全部):所有元素同时处理,顺序无关紧要,所以可以彻底并行。
reduce 归约
reduce 把许多食材“炖”成一锅汤:求和、求最大值、求均值都用它。GPU 上的秘诀是分层的两两相加——先让 N 个帮厨两两配对,把结果合并,只剩一半,再继续配对合并,直到炖成一锅。这样并行深度只有约 log₂(n) 轮,而不是串行的 n 次加法。
1. x_i(第 i 个元素):第 i 份食材,是归约的输入。
2. 求和符号(Σ i=0 到 n-1):把所有食材都倒进同一口锅里。
3. 两两相加的展开式:并行归约的秘诀——两两同时进行加法,而不是一个一个累加。
4. 并行深度(约 log₂ n):串行要 n-1 次加法,并行只需要约 log₂(n) 轮合并。
sort 排序
sort 按大小把食材码放整齐。Thrust 底层封装了高度调优的排序 kernel(CUDA 后端常基于 CUB 的基数排序与归并排序组合),对大规模数组,性能接近手写最优实现。调用只有一行,复杂度为经典的 O(n log n)。
1. T(n)(耗时随规模增长):把 n 份食材排整齐所需的操作次数。
2. O(n log n)(复杂度级别):规模翻倍时,耗时略超两倍;这是通用比较排序的理论下界附近。
3. 托底保障:Thrust 不要求你关心内部用基数排序还是归并排序,只保证这个复杂度级别。
三道菜可以自由组合:饭店流水线常常“先变换、再归约、最后排序取 TopK”。厨师长对每条流程都胸有成竹,你只管点菜。
迭代器体系
帮厨们怎么知道“这份是牛肉、那份是豆腐”?靠的是食材上的标签纸。Thrust 的迭代器就是贴在数据上的标签。device_vector 返回的迭代器内部包装了设备指针(thrust::device_ptr),它说明“这份食材在后厨冷藏柜的哪个架子上”——比裸指针更安全,因为它知道数据类型和所属容器。
除了普通迭代器,Thrust 还有几张“魔法标签”:constant_iterator 让每个位置都读到同一个常数,就像所有食材都贴着“特价 5 元”;counting_iterator 自动生成 0、1、2、3…… 的连续序列,省掉你手动造一个下标数组;zip_iterator 则把多个容器的对应元素打包成元组,像把“菜名、价格、图片”三张标签贴在同一份菜单条目上——一次 transform 就能同时处理三份相关数据。
这些特殊迭代器的妙处在于:它们只描述“数据长什么样”,并不会真的把数据搬来搬去。算法按需取值,延迟生成序列,省掉了大量临时数组和中间内存。
1. a_i、b_i(两个序列):两张不同的标签,例如价格序列和库存序列。
2. zip(打包):把同一下标的两个元素合成一个元组,这就是 zip_iterator 的行为。
3. 映射结果 (a_i, b_i):一次遍历里同时拿到两组信息,避免反复访问多个容器。
4. i 的范围:要求两个序列长度一致,配对必须一一对应,否则越界。
有了标签纸,厨师长不需要问你“这份在哪儿”,只需要说“按标签干活”,帮厨就能精准取料、毫不含糊。
执行策略
同样一道菜,你可以让一个帮厨慢慢做,也可以让一百个帮厨同时做——这就是执行策略。Thrust 提供 thrust::seq、thrust::par、thrust::device 三档“人手配置”,作为算法的第一个参数传入,例如 thrust::sort(thrust::device, first, last)。
thrust::seq 表示串行执行:一个帮厨从头做到尾,适合小数据量或调试时看过程。thrust::par 表示允许并行调度,配合后端决定实际执行方式。thrust::device 则明确指定在 GPU 设备上执行——相当于写清楚“这道菜必须在后厨大灶上做”。注意,策略决定的是“怎么执行”(串行还是并行),迭代器指向的设备内存才决定“在哪里取食材”。
策略选择本质上是一笔经济账:调度本身有固定成本。数据量小时,喊话、分派、汇总结果的开销比干活本身还大,不如一个帮厨直接做完;数据量大时,并行分摊的优势才凸显出来。存在一个“拐点”,越过它并行才划算。
1. T_总(实际总耗时):从下单到上菜的真实时间。
2. T_调度(调度开销):厨师长喊话、分派任务、汇总结果的固定成本,与数据量无关。
3. T_工作 / N_线程(分摊后工作量):工作总量被 N 个帮厨平分后的理想耗时。
4. 拐点判断:当 T_调度 远超分摊后的工作量时,并行反而更慢。
人手多不一定更快——如果任务只有一颗土豆,十个帮厨抢着削反而添乱。理解执行策略,就是学会算清楚这笔账。
自定义算子
厨师长的固定菜谱不够用的时候,你需要给他一张即写即用的配方卡:“每份牛排上撒 5 克盐”。Thrust 里的配方卡就是自定义算子,可以用 functor(仿函数)或 lambda(匿名函数)来写。
functor 是一个重载了 operator() 的 struct / class。比如定义一个“平方器”:struct square { __device__ float operator()(float x) { return x * x; } };,然后传给 thrust::transform。C++11 之后更常用 lambda,一行搞定:thrust::transform(dv.begin(), dv.end(), dv.begin(), [](float x){ return x * x; });。lambda 其实就是编译器帮你生成的匿名 functor,殊途同归。
使用时有几个硬性注意点:在 GPU 上执行的算子必须用 __host__ __device__ 标记;lambda 捕获变量必须按值捕获(设备端访问不了主机内存);不能用 std::vector 这类主机容器去捕获。想象一下:配方卡是发给千里之外的帮厨的,卡上只能写“盐 5 克”这种可复制的内容,不能写“去一楼冰箱拿”——他根本够不着。
1. x(入参):传入的每个元素,相当于“每份食材”本身。
2. x² + 1(规则):配方卡上写的具体操作,对应 lambda 的函数体。
3. ↦(映射箭头):表示从输入到输出的对应关系,是变换算法的核心语义。
4. lambda 与 functor 的关系:lambda 会被编译器展开成一个匿名的 functor 类,本质相同。
配方卡要写得清楚、写得短。所有帮厨都会照着它执行,写错一处就是一万份菜全部做错——所以自定义算子要精心设计,先在 CPU 上验证逻辑,再搬上 GPU。
性能对比
有人觉得手写 kernel 像“自己挽袖子进厨房”,更精细、更可控;Thrust 像“交给厨师长”,省事但怕大锅饭不好吃。真实对比到底如何?结论是:看场景。
对 sort、reduce、scan 这类已有成熟并行算法的操作,Thrust 底层调用高度调优的实现(CUDA 后端基于 CUB),性能通常与手写最优接近,甚至更优——毕竟库作者花了多年打磨,还针对不同数据规模做了分派。手写 kernel 的赢面在于:你可以针对特定访存模式、特定数据分布(比如稀疏、重复度高)做极致定制,避开 Thrust 的通用模板开销。
另一个被忽略的成本是“启动开销”:小规模、多次启动的场景下,每次调用都要付出调度成本,手写一个融合的大 kernel 可能更划算。Amdahl 定律为这一切划定了上限:无论你有多少线程,串行部分始终卡住加速比。
1. P(并行比例):整个流程中能被帮厨们同时做的那部分占比。
2. 1 - P(串行比例):必须一个人完成的部分,比如拷贝、汇总结果、依赖步骤。
3. N(线程数):帮厨人数,也就是参与并行的处理器数量。
4. S(加速比):最终快了多少倍;当 N 趋近无穷时,S 收敛到 1/(1-P),串行部分成了天花板。
厨师长的价值在于“用 80% 的代码量拿到 90% 的手写性能”。当性能瓶颈清晰、访存模式特殊时,再亲自下场手写不迟——但默认,请相信厨师长。
实战应用
最终,一切要落到一套完整的后厨流程上:下单(CPU 数据)→ 搬进冷藏柜(device_vector)→ 厨师长做菜(算法链)→ 端回前台(回传 CPU)。把这套流程固化下来,就是 Thrust 的典型工作流。
实战中最常见的模式是:先用 thrust::copy 把原始数据从 host_vector 拷进 device_vector;然后链式调用 transform(归一化)→ reduce(求统计量)→ sort(排序)→ 截断取 TopK;最后把结果拷回主机。每个环节都尽量留在设备端,避免中途回传。比如:
常见陷阱也值得记住:频繁启动小数组,调度开销会吞噬并行收益;忘记 copy 就直接用主机指针访问设备内存,程序崩溃或读到垃圾值;lambda 捕获不当导致编译错误。调试技巧也很简单:把设备结果拷回主机分段打印,缩小范围对比;用 cuda-memcheck 检查越界访问。把“最小复现”思想用在 GPU 上,问题往往一目了然。
1. CPU 数据(前台订单):最初位于主机内存的原始数据。
2. → copy(搬运):用 thrust::copy 把数据搬进设备向量,这是唯一一次大搬运。
3. GPU 显存(后厨):搬入之后,所有算法都在设备端连续执行,不回传。
4. → copy 回 CPU(上菜):把最终结果传回主机,整个过程只搬两次。
把这套流程练熟,你就从“只会自己切菜的初学者”变成了“能指挥整间厨房的掌勺”。以后无论排序、统计还是推荐打分,都可以沿用同一套点菜方式——这就是 Thrust 带给你的职业底气。
thrust::device_vector<float> scores = host_scores;thrust::transform(scores.begin(), scores.end(), scores.begin(), normalize);float mean = thrust::reduce(scores.begin(), scores.end()) / scores.size();thrust::sort(scores.begin(), scores.end(), thrust::greater<float>());
AI 应用场景
推荐系统 CTR 打分后的 TopN 排序
阶段:模型推理后处理
解决的问题:线上推荐要对百万级候选商品打分后取前 100,传统方式在 CPU 上用 std::sort 逐个排序,当 batch 很大时时延感人。用 Thrust 把打分数组放上 GPU,thrust::sort_by_key 按分值排序后直接截断,吞吐可以提升数倍——这就好比把“一盘菜一盘菜地摆盘”变成“厨师长一声令下,全体帮厨同时摆一百桌”。
具体案例:某广告平台的粗排结果重排,batch 内 100 万候选分数用 thrust::sort_by_key 排序后截断取前 100,端到端时延从 8ms 降到 2ms,且代码只有几行,维护成本极低。
大模型推理中的 Embedding 归一化与统计
阶段:预处理与后处理
解决的问题:向量检索服务需要给海量 embedding 做 L2 归一化、计算 batch 均值方差、按置信度过滤,传统逐条循环在 CPU 上慢且浪费。用 thrust::transform 对每个向量除以其模长(模长可由 thrust::reduce 预先求出),用 copy_if 过滤异常向量,全部在 GPU 设备端完成。
具体案例:某向量检索服务的 embedding 归一化管线,用 transform + reduce + 链式调用替代 Python for 循环后,单 batch 处理耗时从 15ms 降到 3ms,同时代码可读性更高。
训练管线中的特征清洗与标准化
阶段:特征工程
解决的问题:训练数据动辄几十万、上百万样本,特征清洗(填充缺失值、min-max 缩放、过滤异常样本)若用 Python 循环处理,几个小时都跑不完。把数据搬上 GPU 后,用 transform 做缺省值填充与缩放、reduce 求全局统计量、copy_if 过滤异常,整个预处理变成“一次搬运 + 三次算法调用”。
具体案例:某推荐模型训练管线,用 Thrust 风格的 GPU 预处理替代 Pandas 逐行操作后,特征构建耗时从 40 分钟降到 4 分钟,且内存占用更稳定。
Python 演示
# -*- coding: utf-8 -*-"""模拟 Thrust 风格链式调用: transform -> reduce -> sort"""import math class DeviceVector: """极简版 thrust::device_vector 模拟: 数据常驻"设备端"(这里就是 list)""" def __init__(self, data): self._data = list(data) def transform(self, fn): # 对应 thrust::transform, 逐元素应用函数 return DeviceVector([fn(x) for x in self._data]) def reduce(self, fn): # 对应 thrust::reduce, 两两归约成一个值 acc = self._data[0] for x in self._data[1:]: acc = fn(acc, x) return acc def sort(self, reverse=False): # 对应 thrust::sort return DeviceVector(sorted(self._data, reverse=reverse)) def to_host(self): # 对应 thrust::copy 回 host return self._data def main(): scores = [0.2, 8.1, 5.5, -1.3, 9.9, 3.2] # 数据搬上 GPU(设备向量) dv = DeviceVector(scores) # transform: 逐元素求 exp, 相当于配方卡 probs = dv.transform(lambda x: math.exp(x)) # reduce: 求和后再归一化 total = probs.reduce(lambda a, b: a + b) norm = probs.transform(lambda p: p / total) # sort: 按概率从大到小排序 ranked = norm.sort(reverse=True) # 数据拷回 CPU(主机向量) top3 = [round(p, 4) for p in ranked.to_host()[:3]] print("softmax 后概率 Top-3:", top3) assert abs(sum(ranked.to_host()) - 1.0) < 1e-6 if __name__ == "__main__": main()
class DeviceVector:模拟thrust::device_vector,数据在“设备端”常驻。def __init__(self, data): self._data = list(data)底层存储就是一份数据列表。def transform(self, fn):对应thrust::transform,逐元素应用函数。return DeviceVector([fn(x) for x in self._data])生成一个新的设备向量,返回新向量而非原地乱改。def reduce(self, fn):对应thrust::reduce,把整个序列归约成一个值。probs = dv.transform(lambda x: math.exp(x))lambda 就是正文里的“配方卡”。total = probs.reduce(lambda a, b: a + b)归约求和,对应 GPU 上的并行加法树。norm = probs.transform(lambda p: p / total)归一化,全部留在“设备端”完成。ranked = norm.sort(reverse=True)排序取 Top,对应thrust::sort。top3 = [round(p, 4) for p in ranked.to_host()[:3]]只有最后这一步才“拷回主机”。
常见误区
误区我 C++ 很熟,就能直接上手 Thrust,完全不用了解线程块、显存、内存模型这些概念,就当它是一个跑在 GPU 上的 std 库。
正解Thrust 隐藏了 kernel 的编写,但没有隐藏硬件差异。你必须知道数据到底在主机内存还是显存、拷贝开销有多大、执行策略会不会影响性能。否则写出“每步算法都来回搬数据”的程序,可能比纯 CPU 还慢——厨师长再厉害,也架不住你把食材每两分钟搬一次楼。
为何常见:STL 接口太像 std 了,让人误以为背后没有硬件边界,这是“接口友好”带来的认知幻觉。
误区用了 GPU 就一定快,`device_vector` 是万能加速器,把数据往里一扔就完事。
正解快慢取决于数据是否常驻设备端。每次主机↔设备拷贝都要走 PCIe 总线,带宽有限;数据量小或者频繁搬运时,传输时间远超计算时间,整体可能比 CPU 还慢。正确做法是“一次搬入、多次计算、一次搬出”,让数据在冷藏柜里住久一点。
为何常见:GPU 算力宣传深入人心,但“CPU 到 GPU 的路程”(数据传输)总是被忽略,路费往往比菜钱贵。
误区库是通用代码,自动生成的东西肯定打不过我为这个场景手工打磨的 kernel。
正解对 sort、reduce、scan 这类成熟原语,Thrust 底层封装了 CUB 等高度调优的实现,性能通常与手写最优相当,有时还更快。手写的赢面只在特殊访存模式或需要多操作融合的极端场景。你可以先测一下再下结论,别为了“手写优越感”丢掉开发效率。
为何常见:人们高估自己 kernel 的优化程度,低估了库作者多年针对各种规模、各种架构的打磨。
误区我在 lambda 里捕获了外层 `std::vector` 和引用,写起来很顺手,应该没问题吧。
正解设备端函数访问不了主机内存。lambda 捕获基本类型(按值)没问题,但捕获 std::vector、捕获引用、捕获指针指向主机对象,都会在编译或运行时出错。而且算子需要 __host__ __device__ 修饰才能被 nvcc 编译进设备代码——配方卡只能写“可随身携带的调料”,不能写“去一楼冰箱取”。
为何常见:主机端 lambda 用习惯了,忘了这段代码会被 nvcc 编译成设备代码,运行环境完全不同。
误区`thrust::seq` 就是强制走 CPU,`thrust::device` 才走 GPU,两个概念是一对。
正解执行策略决定的是“执行语义”而不是“执行位置”:seq 表示串行执行(在设备迭代器上就是单线程地在设备上跑),device 表示在 GPU 设备上并行执行。数据到底在哪个设备上,由迭代器指向的存储决定——device_vector 的迭代器在哪儿运行都指向显存。把“串行/并行”和"CPU/GPU"两对概念分开,才算真正理解策略。
为何常见:策略名称带有 device 字样,容易让人误以为其他策略都不碰 GPU,其实是两码事。
面试题
Q1. Thrust 库在 CUDA 生态中的定位是什么?
- A.替代 CUDA 的全新编程语言
- B.基于 CUDA 的 STL 风格高级库← 正确答案
- C.仅做矩阵乘法的数学计算库
- D.用于 Python 的绘图工具库
答案:B。Thrust 是 NVIDIA 提供的高层模板库,提供类似 C++ STL 的容器与算法接口,底层仍然调用 CUDA,因此 B 正确。它不是一个新语言(A 错),也不是只做矩阵乘法或绘图(C、D 错)。就像厨师长不是新锅,而是调度锅具和帮厨的人。
Q2. 希望数据在 GPU 显存上被算法处理,应该使用哪种容器?
- A.使用标准容器 std::vector
- B.使用 thrust::host_vector
- C.使用 thrust::device_vector← 正确答案
- D.使用 C 语言裸指针数组
答案:C。thrust::device_vector 的数据放在显存中,算法直接在设备端处理,对应“后厨冷藏柜”。std::vector 和 host_vector 都在主机内存,裸指针不标明设备位置,容易出错。
Q3. 下列哪个 Thrust 算法适合做“逐元素变换”?
- A.sort 对容器元素进行排序
- B.reduce 对元素做归约
- C.transform 逐元素变换← 正确答案
- D.copy 完成元素拷贝
答案:C。transform 对每个元素独立应用同一个函数,是逐元素变换的专用算法,正如切丝机对每根胡萝卜执行相同的操作。sort 是排序、reduce 是合并、copy 是搬运,都不是“逐元素变换”。
Q4. thrust::reduce 最适合完成下面哪项任务?
- A.计算数组所有元素的总和← 正确答案
- B.对数组元素进行快速排序
- C.在不同设备之间搬运数据
- D.生成连续整数的递增序列
答案:A。reduce 通过两两合并把整个序列归约成一个值,最适合求和、求最大、求均值——相当于把一万份食材炖成一锅汤。排序用 sort,搬运用 copy,生成序列用 counting_iterator。
Q5. 把 device_vector 的数据传回 CPU 侧,常见做法是?
- A.使用 thrust::copy 拷回主机← 正确答案
- B.直接解引用设备端指针
- C.让 device_vector 做自动同步
- D.把设备向量转成 C 风格数组
答案:A。正确姿势是用 thrust::copy(dv.begin(), dv.end(), hv.begin()),把数据从设备向量拷回 host_vector。设备端指针在主机上直接解引用会崩溃;device_vector 不会自动同步;转成 C 数组依然是指向显存的指针,主机照样访问不了。
Q6. counting_iterator 的主要作用是什么?
- A.生成连续的整数索引序列← 正确答案
- B.统计容器内元素总个数
- C.获取迭代器当前的位置
- D.定位容器末尾的位置
答案:A。counting_iterator 会在遍历时自动生成 0、1、2…… 的连续序列,省去手动创建下标数组的麻烦。它不负责统计数量、获取位置或定位末尾,那些是别的迭代器接口的职责。
Q7. 关于 thrust::device 执行策略,下列说法正确的是?
- A.指定算法在 GPU 上并行执行← 正确答案
- B.强制算法在 CPU 上执行
- C.禁用多线程并行能力
- D.控制线程块数量上限
答案:A。thrust::device 明确把算法派到 GPU 设备上并行执行,相当于写清楚“这道菜必须用后厨大灶做”。它不会强制 CPU、禁用并行或手动控制线程块——线程块管理由库内部完成,正是“免写 kernel”的体现。
Q8. 下列哪些属于 Thrust 的核心算法?
- A.sort 算法完成排序← 正确答案
- B.reduce 算法对元素归约← 正确答案
- C.transform 算法逐元素变换← 正确答案
- D.借助 CUB 低层库完成排序
答案:ABC。sort、reduce、transform 是 Thrust 对外提供的三大核心算法,对应排序、归约、逐元素变换。CUB 是 Thrust 底层可能调用的低层库,并不属于 Thrust 对外算法接口——就像帮厨用的刀属于刀具供应商,不是厨师长的招牌菜。
Q9. 在 CUDA 中使用自定义 functor 需要注意哪些要求?
- A.重载带参的 operator()← 正确答案
- B.使用 host device 修饰← 正确答案
- C.必须继承虚拟基类才行
- D.捕获变量须按值传入设备← 正确答案
答案:ABD。functor 必须重载 operator() 以便算法调用它;标记 __host__ __device__ 才能同时被主机和设备编译;lambda 捕获必须按值,因为设备端访问不了主机内存。functor 不需要继承任何基类(C 错),这正是它轻量灵活的原因。
Q10. 与手写 kernel 相比,Thrust 的典型优势有哪些?
- A.开发效率比手写更高← 正确答案
- B.自带调优的通用算法实现← 正确答案
- C.自动隐藏全部的数据传输
- D.完全替代手写 kernel
答案:AB。Thrust 提供 STL 风格接口,开发效率高;底层封装 CUB 等高度调优实现,通用场景性能接近手写最优。但它不会自动隐藏数据传输(搬运仍需 thrust::copy),也不能完全替代手写 kernel——特殊访存模式仍需手写定制。