目录
并行应用程序中最常见的三种并行层(layers)
- The message-driven layer
- The fork-join layer
- Single Instruction, Multiple Data (SIMD) layer
The message-driven layer
- 将规模较大的计算结构化,通过消息相互通信
- 常见模式包括
- Streaming Graphs
- Streaming Graph Processing
- 一种用于处理实时数据流的计算模型
- 结合了图计算和流式数据处理的概念
- 将图数据作为一个数据流,遍历图数据中的边执行计算的框架
- Data Flow Graphs
- 用 nodes 和 edges 的有向图来描述计算过程
- Dependency Graphs
- 表示实体之间的依赖关系
- Streaming Graphs
- TBB 的 Flow Graph 接口支持这种类型的并行
The fork-join layer
- 面对一个大规模任务,先串行将任务拆分为多个可并行执行的子任务,再进行并行计算 (fork)
- 当所有并行计算的子任务完成后再继续执行串行部分的逻辑(join)
- 常见模式包括
- functional parallelism (task parallelism)
- parallel loops
- parallel reductions
- pipelines
- TBB 通过 Generic Parallel Algorithms 支持
Single Instruction, Multiple Data (SIMD) layer
- 通过同时对多个数据元素应用相同的操作来实现的
- 这种类型的并行性通常使用矢量扩展(例如 AVX、AVX2 和 AVX-512)来实现
- 这些扩展使用每个处理器核心中可用的矢量单元
- VS 中有专门设置使用某类矢量扩展的编译选项
并行设计模式
设计并行算法的建议
- 建议程序员需要通过四个设计空间来开发并行程序
- 寻找并发性 Finding concurrency
- TBB 自动做了负载均衡,我们只需要保证获得一个连续的任务
- 算法结构 Algorithm structures
- 使用并行设计模式设计并行算法
- 支撑结构 Supporting structures
- 从算法设计转入到算法实现
- 考虑如何组织并行程序以及用于管理共享(尤其是可变)数据的技术
- 实施机制 Implementation mechanisms
- 包括线程管理和同步
- TBB 处理所有线程管理,我们只需要关心更高设计级别的任务
- 寻找并发性 Finding concurrency
- 有效的模式
- 实现并行可扩展性的两个先决条件是良好的数据局部性和避免开销
- 将算法策略抽象为语义并且实现已被证明在实践中非常有效的模式
- 这种分离关注的方式使得可以分别推理高层算法设计和底层(通常是特定于机器的)细节
- Data Parallelism Wins 数据并行是最好的并行算法设计策略
- 可扩展并行性的最佳总体策略是数据并行性 (data parallelism)
- 并行性随着问题规模的增长而增强
- 通常,数据被分成块,每个块都通过单独的任务进行处理
- 有时是平均分割的, 有时是递归分割的
- 重要的是更大的数据集会产生更多的任务
- 一般来说,无论问题是规则的还是不规则的,数据并行都可以应用
- 数据并行的对立面是功能分解 (functional decomposition)(也称为任务并行 task parallelism)
- 是一种并行运行不同程序功能的方法
- 功能分解充其量可以将性能提高一个常数因子
- 有时,功能分解可以提供满足性能目标所需的额外并行性
- 但这不应该是主要策略,因为它无法扩展
- 可扩展并行性的最佳总体策略是数据并行性 (data parallelism)
设计模式
嵌套模式 (Nesting Pattern)
- 因嵌套支持而得到的结果有两个含义
- 在选择是否应该调用 TBB 模板时,不需要知道我们是处于“并行区域”还是“串行区域”
- 由于使用 TBB 只是创建任务,所以不必担心线程的超额订阅
- 不需要担心调用 TBB 编写的库,以及控制它是否可以使用并行性
- 在选择是否应该调用 TBB 模板时,不需要知道我们是处于“并行区域”还是“串行区域”
- 嵌套可以被认为是一种元模式 (meta-pattern)
- 因为它意味着模式可以分层组合
- 有模块化和可组合性的嵌套模式是在设计 TBB 时思考的关键
Map Pattern
- 此模式所表达的独立性使其具有很好的可扩展性
- Map Pattern 是并行编程的最佳模式
- 将工作划分为统一的独立部分,这些部分并行运行,没有依赖性
- 这代表了一种常规并行化 (regular parallelization )
- 称为尴尬并行化 (embarrassing parallelism)
- Map Pattern 值得在任何可能的情况下使用
- 因为它允许高效的并行化和高效的矢量化
- Map Pattern 不涉及各部分之间共享的可变状态
- 映射函数(独立的工作部分)必须是 “纯粹的”,因为它不能修改共享状态
- std::share_ptr 可能具有共享含义
- parallel_for 适用于 Map Pattern
- parallel_invoke 可用于少量映射类型的并行化
- 但有限的数量不会提供太多可扩展性
工作堆模式 (Workpile Pattern)
- 一种广义的 Map Pattern
- 每个实例(映射函数)可以生成更多实例
- 工作可以添加到要做的 “一堆” 事情中
- 如,可以用于树的递归搜索,希望生成实例来处理树的每个节点的每个子节点
- 工作堆模式比映射模式更难矢量化
- parallel_for_each 适用于工作堆模式
Reduction Patterns (Reduce and Scan) 归约模式(归约和扫描)
- 可以被认为是一个映射操作
- 其中每个子任务都会产生一个子结果
- 需要将其组合起来形成最终的单一答案
- 使用关联的 “组合器函数” (combiner function) 来组合多个子结果
- 由于舍入变化,浮点数计算会有精度误差
- parallel_reduce 适用于 Reduction Pattern
- parallel_definistic_reduce 更加精确
- 典型的组合器
- 加法、乘法、最大值、最小值、以及布尔运算 AND、OR 和 XOR
- 扫描模式
- 并行计算前缀
- 具有固有串行依赖性的场景中非常有用
- 先拆分数据,并行-reduce,再基于 reduce 结果进行并行-reduce
- parallel_scan 适用于 Scan Pattern
- 寻找最大值 parallel_reduce 实现
int maxValue = tbb::parallel_reduce( tbb::blocked_range<int>(0, a.size()), std::numeric_limits<int>::min(), [&](const tbb::blocked_range<int>& r, int init) -> int { for(int i = r.begin(); i != r.end(); ++i) { init = std::max(init, a[i]); } return init; }, []
C++并行应用程序设计与优化

最低0.47元/天 解锁文章
1290

被折叠的 条评论
为什么被折叠?



