C++高性能并行编程与优化 - 课件 - 06 TBB 开启的并行编程之旅
Toolkit 10.0 以上( GPU 专题) 第 0 章:从并发到并行 摩尔定律:停止增长了吗? • 晶体管的密度的确仍在指数增长,但处理器主 频却开始停止增长了,甚至有所下降。 • 很长时间之前我们就可以达到 2GHz ( 2001 年 8 月),根据 2003 年的趋势,在 2005 年 初我们就应该研发出 10GHz 的芯片。 • 可为何直到今天也生产不出 10GHz 的芯片? 是性能毕竟不是线性增长。 • 为什么无法做到呢?首先,为了保证缓存一致性以及其他握手协议需要运行时间开销。在 今天,双核或者四核机器在多线程应用方面,其性能不见得的是单核机器的两倍或者四倍。 这一问题一直伴随 CPU 发展至今。 并发和并行的区别 • 运用多线程的方式和动机,一般分为两种。 • 并发:单核处理器,操作系统通过时间片调 度算法,轮换着执行着不同的线程,看起来 就好像是同时运行一样,其实每一时刻只有 章:并行循环 时间复杂度( time-efficiency )与工作量复杂度( work-efficiency ) • 在“小学二年级”算法课里,我们学过复杂度的概念,意思是算法执行所花费的时间取决于数据量 的大小 n ,比如 O(n²) 表示花费时间和数据量的平方成正比。 • 对于并行算法,复杂度的评估则要分为两种: • 时间复杂度:程序所用的总时间(重点) • 工作复杂度:程序所用的计算量(次要)0 码力 | 116 页 | 15.85 MB | 1 年前3Rust与算法 - 谢波
其他 的留给时间检验。不懂就学,技术写作更像一种共创, 要反复总结和修改 ( 费曼学习法 ) 。 写作本书给我的启示 基础、排序、查找、树、图 代码框、颜色、图片绘制均由 Latex 完成 可参考点 为什么 为什么讲这个话题? 为什么要讲数据结构和算法两部分? 算法相关知识 算法相关知识 • 抽象数据类型 • 时空复杂度 • 复杂度计算 • 基本数据结构复杂度 抽象数据类型 抽象数据类型 什么是抽象数据类型? 为什么需要抽象数据类型? 时空复杂度 • 时间复杂度更被看重 • 时间和空间复杂度不是对立的,可以协同 时间和空间复杂度 复杂度计算 • 大O标记法(数量级近似) • 用 AI 来估计 算步骤、算存储 Rust 基本数据结构复杂度 线性数据结构 非线性数据结构 总体来看,时间复杂度没有超过 O(n) 的! Rust 实现数据结构 • 栈 • 联想:图数据结构的的点、边、面似乎满足欧拉公式 : V – E + F = 2 、则时间复杂度为: O(V+E) = O(2E – F + 2) • V = 14; E = 18; F = 5 + 1; • V+E = 32 • 2E – F + 2 = 32 总结及学习资源 • 算法总结 • 学习资源 总结及学习资源 Rust 算法总结 • 复杂度分析及算法优化 • 别自己实现,用标准库 • 利用0 码力 | 28 页 | 3.52 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 17 由浅入深学习 map 容器
类型在栈上的空间就要消耗 32 字节,更不用说可能堆上还有),深拷贝一下要花费不少 时间。 • for (auto [k, v]: m) { • print(k, v); • } map 的遍历:不修改也建议加引用 k v (假如非常大的话) map 中的 堆空间 执行你这段代码 的栈空间 & ( 深拷贝,浪费时间 ) v (假如非常大的话) • 其实,就算遍历时不修改,还是建议加引用,在 类型尺寸很大时,可以节省性能 。 • 因为引用最多只有 8 字节(指针的大小),而他指向的 V 可能是非常大的(比如 string 类型在栈上的空间就要消耗 32 字节,更不用说可能堆上还有),深拷贝一下要花费不少 时间。 • for (auto &[k, v]: m) { • print(k, v); • } map 的遍历:不修改也建议加引用 k v (假如非常大的话) 执行你这段代码 的栈空间 1 次比较运算就能找到(例如查找 1 )。 • 最坏需要 n 次比较运算才能找到或者发现找不到(例如查找 7 ,或者找一个不存在的 数)。 • 所以我们说 vector 查找指定元素的最坏复杂度为 O(n) 。 1 4 2 8 5 7 要找的数 内存 5 ==? 地址 a a+1 a+2 a+3 a+4 a+5 set 查找为什么高效 • set 又称集合(数学概念),是专为查找优化的容器,查找元素要用他自带的0 码力 | 90 页 | 8.76 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 性能优化之无分支编程 Branchless Programming
。流水线的目的是能把原本 串行的一系列指令并行化。为了理解为什 么需要流水线,我们先反过来,假设没有 流水线,会有什么坏处。 • 例如,右边你今天早上的任务清单。 • 请问你这些任务总共需要多少时间? 任务 时间 占用资源 洗脸 5 分钟 眼睛,嘴巴,手 烧开水 10 分钟 煤气灶 刷牙 5 分钟 嘴巴,手 看比站 15 分钟 眼睛 吃饭 30 分钟 嘴巴,手 拉粑粑 20 分钟 屁股 干瞪眼,什么也不做,其实完全可以在烧 开水的同时洗脸刷牙呀!原始的 CPU 也 是这样, ALU 在运算的时候指令解码单元 就在旁边干瞪眼,要等 ALU 跑完写回寄 存器来指令解码单元才开始继续工作,很 低效。 任务 时间 占用资源 洗脸 5 分钟 眼睛,嘴巴,手 烧开水 10 分钟 煤气灶 刷牙 5 分钟 嘴巴,手 看比站 15 分钟 眼睛 吃饭 30 分钟 嘴巴,手 拉粑粑 20 分钟 屁股 洗脸 烧开水 更高效的办法是,观察每个任务都占用哪些 资源,所占用资源不冲突的可以同时进行, 节省时间。 • 例如洗脸需要眼睛嘴巴手,刷牙需要嘴巴手 ,那么洗脸和刷牙不能同时进行。但是烧开 水只需要占用煤气灶,和洗脸刷牙不冲突, 所以可以一边烧开水一边洗脸刷牙。 • 所以让小彭老师来优化的话,可以只需要 5 + 5 + 10 + 20 = 40 分钟,比你快一倍多。 任务 时间 占用资源 洗脸 5 分钟 眼睛,嘴巴,手 烧开水0 码力 | 47 页 | 8.45 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 02 现代 C++ 入门:RAII 内存管理
造函数,那么您必须同时定义或删除拷贝 赋值函数,否则出错。” C++11 :为什么区分拷贝和移动? • 有时候,我们需要把一个对象 v2 移动到 v1 上。而不需要涉及实际数据的拷贝。 • 时间复杂度:移动是 O(1) ,拷贝是 O(n) 。 • 我们可以用 std::move 实现移动。 • v2 被移动到 v1 后,原来的 v2 会被清 空,因此仅当 v2 再也用不到时才用移动 移动构造函数:缺省实现 • 同样,如果对降低时间复杂度不感兴趣: • 移动构造≈拷贝构造 + 他解构 + 他默认构造 • 移动赋值≈拷贝赋值 + 他解构 + 他默认构造 • 只要不定义移动构造和移动赋值,编译器会自 动这样做。虽然低效,但至少可以保证不出错 。 • 若自定义了移动构造,对提高性能不感兴趣: • 移动赋值≈解构 + 移动构造 注: 降低时间复杂度: O(n) >>> O(1) 提高性能: 解决方案:提前获取原始指针 • 最简单的办法是,在移交控制权给 func 前,提前通过 p.get() 获取原始指针: 解决方案:提前获取原始指针(续) • 不过你得保证 raw_p 的存在时间不超过 p 的生命周期,否则会出现危险的空悬指 针。比如右边这样: 更智能的指针: shared_ptr • 使用起来很困难的原因,在于 unique_ptr 解决重复释放 的方式是禁止拷贝,这样虽然有效率高的优势,但导致使0 码力 | 96 页 | 16.28 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 13 C++ STL 容器全解之 vector
max(n, capacity * 2) 。 • size_t resize(size_t n); vector 容器: reserve 预留一定容量,避免之后重复分配 • 内存分配是需要一定时间的。如果我们程序员能预料到数组 最终的大小,可以用 reserve 函数预留一定的容量,这样之 后就不会出现容量不足而需要动态扩容影响性能了。例如这 里我们一开始预留了 12 格容量,这样从 5 我们知道 push_back 可以往尾部插入数据,那么如何往头 部插入数据呢?用 insert 函数,他的第一个参数是要插入 的位置(用迭代器表示),第二个参数则是要插入的值。 • 注意这个函数的复杂度是 O(n) , n 是从插入位置 pos 到 数组末尾 end 的距离。没错,他会插入位置后方的元素整 体向后移动一格,是比较低效的,因此为了高效,我们尽量 只往尾部插入元素。如果需要高效的头部插入,可以考虑用 重复多少次 , 插入的值 ); • 你可能会担心,小彭老师刚刚不是说在头部 insert 是 O(n) 复杂度嘛?那如果再重复 n 次岂不是 O(n²) 复杂度了?不 会哦, insert 的这个重载会一次性批量让 pos 之后的元素 移动 n 格,不存在反复移动 1 格的情况,最坏复杂度仍然 是 O(n) 。如果你自己写个 for 循环反复调 insert 那的确 是会 O(n²) 了,这就是为什么0 码力 | 90 页 | 4.93 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串
则是从尾部开始查找,返回最后一次出现的地方 。 • 例如 “ helloworld”.rfind(‘l’) 会返回 8 ,因为 rfind 是优先 从尾部开始查找的。 • rfind 和 find 的最坏复杂度都为 O(n) ,最好复杂度都为 O(1) 。 find_first_of 寻找集合内任意字符 • size_t find_first_of(string const &s, size_t pos = 0) insert 一 样)。这意味着 replace 最坏是 O(n) 复杂度的,然而如果原来的子字 符串和新的子字符串一样长度,例如 “ helloworld”.replace(4, 2, “pf”) 会 得到 “ helpfworld” 则后面的 “ world” 不需要被平移,是最好的 O(1) 复 杂度……好吧,其实是 O(len) 复杂度, len 就是这里子字符串的长度 2 。 • 此外,要注意 s += “wor” 等价。 • 性能如何? append 的扩容方式和 vector 的 push_back 一样,每次超过 capacity 就预留两倍空间,所以重复调用 append 的复杂度其实是 amortized O(n) 的。 • 函数原型: • string &append(string const &str); // str 是 C++0 码力 | 162 页 | 40.20 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 10 从稀疏数据结构到量化数据类型
写入:如果不存在,则创建该表项 用 unordered_map 来存储 map 基于红黑树,会按照键值排序,需要键值具有 operator< 重载,复杂度 O(logn) C++11 新增的 unordered_map 基于哈希表,不保证顺序但更高效,需要键值能被哈希,复杂度 O(1) 用 unordered_map 按 16x16 分块存储 分块能减少 unordered_map 中存储的表项数量,从而减轻哈 memory- bound )。 • 右边就是一个很好的例子。 使用 int64_t :每个占据 8 字节 • 如果用更大的数据类型,用时会直接提升两倍! • 这是因为 i % 2 的计算时间,完全隐藏在内存 的超高延迟里了。 • 可见,当数据量足够大,计算量却不多时,读写 数据量的大小唯一决定着你的性能。 • 特别是并行以后,计算量可以被并行加速,而访 存却不行。 使用 int8_t Taichi 也支持量化存储了 • https://yuanming.taichi.graphics/publication/2021-quantaichi/quantaichi.pdf 小彭老师第一时间赶到现场锐评 • https://yuanming.taichi.graphics/publication/2021-quantaichi/quantaichi.pdf ← ??? 第 7 章:0 码力 | 102 页 | 9.50 MB | 1 年前3基于 Rust Arrow Flight 的物联网和时序数据传输及转换工具 霍琳贺
Series Database ),专为物联网、工业互联网、金融、 IT 运维监控等场景设计并优化,具有极强的弹性伸缩能力。同时它还带有内建的缓存、流式计算、数据订阅等 系统功能,能大幅减少系统设计的复杂度,降低研发和运营成本,是一个极简的时序数据处理平台。 采用关系型数据库模型 需要建库、建表, 为提升写入和查询效率,要求一个数据采集点一张表 为实现多表聚合,引入超级表概念 com/taosdata/TDengine 全球 50 多个国家安装实例超 270k | GitHub 全球趋势排行榜多次排名第一 TDengine - 数据模型 1. 设备 ID 及关联属性( Tags ) 2. 时间戳 3. 结构化采集量 STable 超级表 Table 子表 CREATE STABLE `meters` ( `ts` TIMESTAMP, `current` FLOAT, GCP) CONTENTS 自 我 介 绍 T D e n g i n e t a o s X R u s t 使 用 taosX - 物联网数据接入问题 • 多种不同协议数据对接,开发复杂度高 • 模块之间关联性不高但模块组成复杂,可维护性差 • 大量设备大量数据归集存储,存储压力大 • 数据总线 / 消息队列消息接入,定制化程度要求高 • 数据业务逻辑自定义需求强 • 一定的实时数据分析能力0 码力 | 29 页 | 2.26 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 14 C++ 标准库系列课 - 你所不知道的 set 容器
那如果我确实需要让 set 迭 代器向前移动 3 格怎么办? • 可以调用三次 ++ 运算,实 现和 + 3 同样的效果。 • vector 迭代器的 + n 复杂度 是 O(1) 。而 set 迭代器模 拟出来的 + n 复杂度为 O(n) 。虽然低效,但至少可 以用了。 std::next 等价于 + • 但是这样手写三个 ++ 太麻烦了 ,而且是就地操作,会改变迭代 器本身。 upper_bound 。 • unordered_set 只适合:按值相等查找,通过函数 find 。 • 小贴士: unordered_set 的性能在数据量足够大(> 1000 )时,平均查找时间比 set 短,但不保证稳定。 • 我个人推荐使用久经沙场的 set ,数据量小时更高效。 关于 set 和 map 还没有讲到的 • unordered_set> 0 码力 | 83 页 | 10.23 MB | 1 年前3
共 23 条
- 1
- 2
- 3