十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

一文读懂 oneTBB 迭代空间高级玩法:自定义 Range、blocked_range2d/3d 与 blocked_nd_range

一文读懂 oneTBB 迭代空间高级玩法:自定义 Range、blocked_range2d/3d 与 blocked_nd_range 一文读懂 oneTBB 迭代空间高级玩法自定义 Range、blocked_range2d/3d 与 blocked_nd_range【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold导读本文围绕 oneAPI Threading Building BlocksoneTBB即当前仓库 third-party/tbb 目录内置的 TBB 版本用户指南中「其他类型的迭代空间」这一高级主题展开讲解如何通过自定义 Range 类型突破 blocked_range 的局限并系统梳理 blocked_range2d、blocked_range3d、blocked_nd_range 三种多维迭代空间的用法与底层分割原理。读完本文你将掌握 Range 概念的最小接口契约、分割构造器的正确写法以及如何用多维范围让 parallel_for 获得更好的缓存局部性。背景为什么需要「其他类型的迭代空间」在此之前指南中的所有并行循环示例都使用blocked_rangeT类来描述迭代空间。blocked_rangeT表示一个半开区间[begin, end)上的连续整数或迭代器范围配合 并行算法入口源码 中实际使用的tbb::parallel_for、tbb::parallel_reduce等算法模板即可完成大部分并行化改造。但blocked_rangeT并非万能的它只能描述一维、线性的迭代空间它无法表达二维图像、三维张量这类天然多维的数据结构某些应用需要把不同类型的索引整数、指针、迭代器组合到同一个迭代空间里。blocked_rangeT的完整定义位于 blocked_range.h其核心接口包括begin()、end()、size()、grainsize()、empty()、is_divisible()以及两个分割构造器。在 blocked_range.h 中可以看到构造时若不显式给出粒度grainsize默认为 1is_divisible()的定义是my_grainsize size()第 85 行即「范围大小超过粒度才允许分割」。TBB 的分区器partitioner正是依赖这套接口来完成递归分割的。因此只要你的自定义类型实现了同样的接口就能被parallel_for等算法模板直接使用——这就是「自定义迭代空间」的全部前提。自定义 RangeRange 概念的最小接口TBB 通过**概念concept**而非继承来约束迭代空间类型任何满足Range 概念的类都可以交给并行算法模板。以你的类名为R为例需要实现以下成员class R { // True if range is empty bool empty() const; // True if range can be split into non-empty subranges bool is_divisible() const; // Splits r into subranges r and *this R( R r, split ); // (optional) Splits r into subranges r and *this in proportion p R( R r, proportional_split p ); ... };对这四个成员的解释empty()当且仅当范围为空时返回true。算法模板用它在递归划分时判断「这个子问题是否还需要处理」。is_divisible()当范围可以被切成两个非空子范围、且这次切分值得付出分割开销时返回true。注意这句话包含了两个条件可切分 切分有利可图。在实践中这通常意味着「范围大小大于某个阈值如 grain size」。基本分割构造器必选R(R r, split)。split是一个哑标记类型tag type第二个参数在函数体内不会被使用它的唯一作用是让编译器把这个构造器与普通的拷贝构造器R(const R)区分开来——两者签名不同重载决议才能找到正确的版本。比例分割构造器可选R(R r, proportional_split p)。支持它的 Range 可以被分区器按比例切分用于处理负载不均的场景。基本分割构造器的语义基本分割构造器应当把r大致切成两半更新后的r成为前半部分新构造出的对象即*this成为后半部分。split与proportional_split类型的定义位于 detail/_range_common.hsplit是一个空类proportional_split携带left()/right()两个比例值默认构造时二者均为 1即等价于对半分割并且提供了explicit operator split()转换——当某个 Range 不支持比例分割时分区器可以退化为基本分割。比例分割构造器的语义与基本分割构造器不同比例分割构造器按p.left() : p.right()的比例划分r更新后的r对应比例的左侧部分构造出的对象对应比例的右侧部分。proportional_split的两个方法left()和right()返回比例值第 47-48 行实现者应当据此计算分割点。两个分割构造器的共同约定无论哪种分割构造器都必须保证更新后的r非空新构造出的对象也非空。并行算法模板只会在r.is_divisible()为真时调用分割构造器用户指南原文明确说明因此在is_divisible()实现正确的前提下这个非空约定是自然可满足的。作为参照blocked_range.h 中blocked_range的基本分割实现为middle begin (end - begin) / 2随后r.my_end middle使原范围成为左半段返回的middle作为新范围右半段的起点。参数解析为什么第二个参数必须按值传split有人可能会问为什么指南示例中写的是R(R r, split)而 blocked_range.h 里写的是blocked_range(blocked_range r, split)因为split是空类型按值传参不产生任何拷贝开销且能自然地区别于拷贝构造器R(const R)后者第一参数是const引用前者第一参数是非常量引用R因为要修改r。如果写成const R你就无法更新r的左半部分分割逻辑无从谈起。blocked_range2d二维迭代空间与最长轴分割blocked_range2dRowValue, ColValue把迭代空间扩展为行 × 列的二维结构定义位于 blocked_range2d.h。它的模板参数允许行和列使用不同的值类型第 34 行即第一维行可以是int、指针或迭代器中的一种第二维列可以独立地选择另一种类型。这意味着你可以把「整数行号 容器迭代器」这样的混合索引组合进同一个迭代空间。构造方式与访问方法blocked_range2d提供两组构造器// 完整形式每维都显式给出粒度 blocked_range2d( RowValue row_begin, RowValue row_end, size_type row_grainsize, ColValue col_begin, ColValue col_end, size_type col_grainsize ); // 简写形式每维粒度取默认值 1 blocked_range2d( RowValue row_begin, RowValue row_end, ColValue col_begin, ColValue col_end );通过rows()和cols()两个方法可以取回代表各行、各列的blocked_range对象第 87-90 行进而像使用一维范围那样调用begin()/end()。分割策略沿最长轴切分blocked_range2d的分割构造器试图沿其最长的那条轴切分。do_split的实现第 94-100 行用了一个比较式my_rows.size() * cols.grainsize() my_cols.size() * rows.grainsize()不等式成立则切列否则切行。这个比较并不是单纯比大小而是把每维的粒度也计入权重——选择「粒度归一化后更长」的轴来切从而避免把某一维切到比粒度还小的碎片。分割时调用的是blocked_range::do_splitblocked_range.h基本分割取中点(end - begin) / 2比例分割则按right() / (left() right())计算右段长度。缓存友好为什么单核也可能更快与parallel_for搭配时blocked_range2d会造成循环的**「递归阻塞」recursively blocked**分割永远沿着当前子范围的最长轴进行每个工作块都尽量保持为接近正方形的块而非拉成一条细长的行或列。这种形状对缓存更友好处理二维数组时细长的条带会导致缓存行被反复重载同一行数据被多个并行块跨缓存行访问近似方形的块让每个线程在局部范围内顺序访问连续内存。因此用户指南明确指出即使只有单处理器用parallel_for遍历blocked_range2dT也可能比顺序等价代码更快——因为这不是并行化带来的收益而是内存访问模式改善带来的收益。blocked_range3d三维迭代空间的扩展blocked_range3d类模板把同样的思路推广到三维在rows()、cols()之外新增了pages()作为第一维维度顺序为pages()—— 页rows()—— 行cols()—— 列其头文件 blocked_range3d.h 中的实现模式与blocked_range2d完全一致empty()在任意一维为空时返回真is_divisible()在任意一维可分时返回真分割同样选择「粒度归一化后最长」的轴。从 blocked_range.h 的友元声明可以看到blocked_range2d和blocked_range3d都通过友元关系访问blocked_range的内部成员来完成分割。blocked_nd_range任意维度的统一抽象如果你需要四维、五维乃至更高维度的迭代空间blocked_range2d/blocked_range3d的「逐维手写」方式就不够用了。blocked_nd_rangeT, N类模板提供任意维度 N的统一方案声明于 blocked_rangeNd.h。与 2D/3D 范围的三点关键差异用户指南特别强调了blocked_nd_range与 2D/3D 范围的两处命名与用法差异结合源码可以归纳为三点所有轴使用同一种值类型模板签名是blocked_nd_rangeValue, NN个维度共享Value第 52-58 行不像blocked_range2d那样行、列可分别指定类型。构造参数是 N 个blocked_rangeValue对象你需要为每个维度显式构造一个blocked_rangeT再传入而不是直接传起止值。构造器通过index_sequence技巧自动生成恰好接收 N 个参数的版本第 72 行因此支持花括号初始化列表逐维书写。通过dim(i)方法取各维范围blocked_nd_range没有pages()/rows()/cols()这种语义化命名而是用dim(unsigned int dimension)按下标访问第 i 维的blocked_range第 78-81 行。内部实现要点从 blocked_rangeNd.h 的源码可以看到几个值得注意的实现细节blocked_nd_range是对blocked_rangeNd_impl的模板别名第 135-136 行而非派生类这是为了让带花括号实参的构造器能够在模板实参推导中正常工作各维范围存储在std::arrayblocked_rangeValue, N中第 113 行empty()用std::any_of判断是否存在空维度任一维为空则整体为空is_divisible()判断是否存在可分维度第 88-99 行分割时用std::max_element找到「粒度归一化后最长」的维度并只切这一维第 120-132 行与 2D/3D 版本「沿最长轴切分」的策略保持一致该头文件在未定义宏TBB_PREVIEW_BLOCKED_RANGE_ND时会直接报错第 20-22 行说明blocked_nd_range在本文所述 TBB 版本中属于预览preview特性使用前需自行定义该宏。实战示例三维卷积与 blocked_nd_rangeint, 3用户指南附带了一个完整示例源码位于 examples/blocked_nd_range_example.cpp。该示例演示了在特征图feature maps上计算三维滤波器的过程对每个输出单元用kernel3d函数把来自特征图某邻域内的值累加求和。示例代码#include oneapi/tbb/blocked_nd_range.h #include oneapi/tbb/parallel_for.h templatetypename Features float kernel3d(const Features feature_maps, int i, int j, int k, int kernel_length, int kernel_width, int kernel_height) { float result 0.f; for (int feature_i i; feature_i i kernel_length; feature_i) for (int feature_j j; feature_j j kernel_width; feature_j) for (int feature_k k; feature_k k kernel_width; feature_k) result feature_maps[feature_i][feature_j][feature_k]; return result; } templatetypename Features, typename Output void convolution3d(const Features feature_maps, Output out, int out_length, int out_width, int out_heigth, int kernel_length, int kernel_width, int kernel_height) { using range_t oneapi::tbb::blocked_nd_rangeint, 3; oneapi::tbb::parallel_for( range_t({0, out_length}, {0, out_width}, {0, out_heigth}), { auto out_x out_range.dim(0); auto out_y out_range.dim(1); auto out_z out_range.dim(2); for (int i out_x.begin(); i out_x.end(); i) for (int j out_y.begin(); j out_y.end(); j) for (int k out_z.begin(); k out_z.end(); k) out[i][j][k] kernel3d(feature_maps, i, j, k, kernel_length, kernel_width, kernel_height); } ); }逐段解读using range_t oneapi::tbb::blocked_nd_rangeint, 3显式声明一个以int为值类型、维度为 3 的迭代空间类型。range_t({0, out_length}, {0, out_width}, {0, out_heigth})以三个花括号初始化列表构造范围每个{begin, end}都会隐式转换为一个blocked_rangeint分别对应输出张量的长、宽、高三个维度。out_range.dim(0)/dim(1)/dim(2)取出三个维度各自的blocked_rangeint再像普通一维范围一样用begin()/end()获取循环边界。body 函数内部的三重嵌套循环把收到的 3D 子范围拆成三个一维区间逐单元计算每个单元调用kernel3d完成邻域求和结果写回输出张量。示例的验证方式示例的main函数第 60-91 行还给出了一个可运行的验证流程特征图尺寸取128 × 16 × 16核尺寸取9 × 5 × 5则输出尺寸按特征图尺寸 − 核尺寸 1推算为120 × 12 × 12特征图全部初始化为1.0f此时每个输出单元的正确结果恒为kernel_length * kernel_width * kernel_height 225卷积完成后对输出逐单元断言k expected用等价性检查验证并行结果正确性。运行前提该示例使用了blocked_nd_range因此必须在包含头文件之前定义宏TBB_PREVIEW_BLOCKED_RANGE_ND否则 blocked_rangeNd.h 中的#error会直接终止编译。这一点在 conformance_blocked_rangeNd.cpp 的一致性测试中同样可以看到测试文件在第 25 行先#define TBB_PREVIEW_BLOCKED_RANGE_ND 1再包含头文件。一致性测试多维范围的行为契约仓库的 TBB 一致性测试conformance test从另一个角度固化了上述所有行为是理解多维范围语义的绝佳补充资料conformance_blocked_range2d.cpp 验证二维范围的分割正确性、rows()/cols()访问以及不同值类型组合conformance_blocked_range3d.cpp 验证三维范围的pages()/rows()/cols()语义conformance_blocked_rangeNd.cpp 覆盖 N 维范围其中定义了一个只提供operator-、operator、operator的AbstractValueType用来证明blocked_nd_range对值类型的要求是概念式的最小需求——只要你的类型支持减法求距离、比较判空/排序和加偏移就能充当任意维度的值类型无需继承任何基类。此外test_blocked_range.cpp 对一维blocked_range的分割行为进行了单元级验证可以作为理解「基本分割 vs 比例分割」的对照。小结与选择建议迭代空间类型维度各维值类型维度访问方式备注blocked_rangeT1单一类型begin()/end()最基础的一维范围blocked_range2dRow, Col2行、列可不同rows()/cols()支持整数/指针/迭代器混用blocked_range3dPage, Row, Col3页、行、列可不同pages()/rows()/cols()2D 思路的直接推广blocked_nd_rangeT, NN任意所有轴同一类型dim(i)预览特性需定义TBB_PREVIEW_BLOCKED_RANGE_ND选择建议只需一维并行循环 → 直接用blocked_rangeT图像、矩阵等二维结构 →blocked_range2d其「沿最长轴递归分割」的策略天然提升缓存局部性甚至可能在单处理器上快于顺序版本体素、张量等三维结构 →blocked_range3d更高维度或需要以统一的模板方式处理任意维度 →blocked_nd_rangeT, N注意其预览特性身份与「所有轴同类型」的限制。如果你的应用场景需要自定义迭代空间例如不规则的网格、稀疏索引、按负载比例分割的任务请牢记 Range 概念的最小契约empty()、is_divisible()、基本分割构造器R(R, split)三者必选比例分割构造器R(R, proportional_split)可选但推荐实现且所有分割都必须保证分割后的两部分均为非空。【免费下载链接】moldmold: A Modern Linker 项目地址: https://gitcode.com/GitHub_Trending/mo/mold创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表