C++高性能并行编程与优化 - 课件 - 04 从汇编角度看编译器优化从汇编角度看编译器优化 by 彭于斌( @archibate ) 往期录播: https://www.bilibili.com/video/BV1fa411r7zp 课程 PPT 和代码: https://github.com/parallel101/course 高性能并行编程与优化 - 课程大纲 • 分为前半段和后半段,前半段主要介绍现代 C++ ,后半段主要介绍并行编程与优化。 1 1.课程安排与开发环境搭建: cmake 与 git 入门 2.现代 C++ 入门:常用 STL 容器, RAII 内存管理 3.现代 C++ 进阶:模板元编程与函数式编程 4.编译器如何自动优化:从汇编角度看 C++ 5.C++11 起的多线程编程:从 mutex 到无锁并行 6.并行编程常用框架: OpenMP 与 Intel TBB 7.被忽视的访存优化:内存带宽与 cpu 缓存机制 x64 架构下的寄存器模型 通用寄存器: 32 位时代 • 32 位 x86 架构中的通用寄存器有: • eax, ecx, edx, ebx, esi, edi, esp, ebp • 其中 esp 是堆栈指针寄存器,和函数的调用与返回相关。 • 其中 eax 是用于保存返回值的寄存器。 通用寄存器: 64 位时代 • 64 位 x86 架构中的通用寄存器有: • rax, rcx0 码力 | 108 页 | 9.47 MB | 1 年前3
Hello 算法 1.1.0 C++ 版一方面,难以排除测试环境的干扰因素。硬件配置会影响算法的性能。比如在某台计算机中,算法 A 的运行 时间比算法 B 短;但在另一台配置不同的计算机中,可能得到相反的测试结果。这意味着我们需要在各种机 器上进行测试,统计平均效率,而这是不现实的。 另一方面,展开完整测试非常耗费资源。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入 数据量较小时,算法 A 的运行时间比算法 B 短;而在输 图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 文。 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 Tip 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即 使函数是尾递归形式,仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列”0 码力 | 379 页 | 18.47 MB | 1 年前3
Hello 算法 1.0.0 C++版一方面,难以排除测试环境的干扰因素。硬件配置会影响算法的性能。比如在某台计算机中,算法 A 的运行 时间比算法 B 短;但在另一台配置不同的计算机中,可能得到相反的测试结果。这意味着我们需要在各种机 器上进行测试,统计平均效率,而这是不现实的。 另一方面,展开完整测试非常耗费资源。随着输入数据量的变化,算法会表现出不同的效率。例如,在输入 数据量较小时,算法 A 的运行时间比算法 B 短;而在输 图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 文。 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 � 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化, 因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列”0 码力 | 378 页 | 17.59 MB | 1 年前3
Hello 算法 1.0.0b4 C++版/* 平方阶(冒泡排序) */ int bubbleSort(vector&nums) { 2. 复杂度 hello‑algo.com 22 int count = 0; // 计数器 // 外循环:未排序区间为 [0, i] for (int i = nums.size() - 1; i > 0; i--) { // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 树中,我们需要知道一个节点的父节点,这可以通过在节点中保存一 个指向父节点的指针来实现,类似于双向链表。 4. 数组与链表 hello‑algo.com 60 ‧ 浏览器历史:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和 后一个网页。双向链表的特性使得这种操作变得简单。 ‧ LRU 算法:在缓存淘汰算法(LRU)中,我们需要快速找到最近最少使用的数据,以及支持快速地添 组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循 环的操作就可以通过循环链表来实现。 ‧ 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用到循环链表。比如在音频、视频播放器中,数 据流可能会被分成多个缓冲块并放入一个循环链表,以便实现无缝播放。 4.3. 列表 数组长度不可变导致实用性降低。在许多情况下,我们事先无法确定需要存储多少数据,这使数组长度的选 择变得 0 码力 | 343 页 | 27.39 MB | 1 年前3
Hello 算法 1.2.0 简体中文 C++ 版图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 文。 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 Tip 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即 使函数是尾递归形式,仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思路更加直观、代码更加易读。以“斐波那契数列” time_complexity.cpp === /* 平方阶(冒泡排序) */ int bubbleSort(vector&nums) { int count = 0; // 计数器 // 外循环:未排序区间为 [0, i] for (int i = nums.size() - 1; i > 0; i--) { // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 0 码力 | 379 页 | 18.48 MB | 10 月前3
C++高性能并行编程与优化 - 课件 - 09 CUDA C++ 流体仿真实战投影部分:扩大一倍 创建与导出 主函数:创建场景 导出 VDB :调用接口 导出 VDB :分离实现 CMake :使用 CUDA 编译器,链接 OpenVDB 在 Blender 中查看导出的结果 边界条件 边界条件:初始化 边界条件:添加判断边界的版本 边界条件:仅在第一层额外判断边界条件 进一步改进 VDB 导出:支持导出多个网格,并指定名称 进一步改进 VDB 导出: P-IMPL P-IMPL 模式 进一步改进 VDB 导出: F-IMPL 模式 Blender 渲染结果 改进 改进边界条件:外部边界流出而不是反弹,内部边界可以流出速度 Blender 中调整一下材质 Blender 中调整一下材质 改进对流:让烟雾随时间逐渐褪色 改进对流:让烟雾随时间逐渐褪色 改进褪色:不是褪色 density ,而是褪色 temperature 改进褪色:不是褪色 density0 码力 | 58 页 | 14.90 MB | 1 年前3
Hello 算法 1.0.0b5 C++版图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出报错。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 文。 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数 是尾递归形式,但仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思路更 平方阶(冒泡排序) */ 第 2 章 复杂度分析 hello‑algo.com 33 int bubbleSort(vector&nums) { int count = 0; // 计数器 // 外循环:未排序区间为 [0, i] for (int i = nums.size() - 1; i > 0; i--) { // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 0 码力 | 377 页 | 30.69 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 01 学 C++ 从 CMake 学起,后半段主要介绍并行编程与优化。 1.课程安排与开发环境搭建: cmake 与 git 入门 2.现代 C++ 入门:常用 STL 容器, RAII 内存管理 3.现代 C++ 进阶:模板元编程与函数式编程 4.编译器如何自动优化:从汇编角度看 C++ 5.C++11 起的多线程编程:从 mutex 到无锁并行 6.并行编程常用框架: OpenMP 与 Intel TBB 7.被忽视的访存优化:内存带宽与 cpu 用户) CMake 3.12 及以上(跨平台作业) Git 2.x (作业上传到 GitHub ) CUDA Toolkit 10.0 以上( GPU 专题) 关于作者 • 我是 Taichi 编译器的贡献者之一( https://github.com/taichi-dev/taichi ) 关于作者(续) • 我是 Taichi Blend 的作者( https://github.com/t 关于作者(再续) • 主导 Zeno 节点仿真框架的开发( https://github.com/zenustech/zeno ) 什么是编译器 • 编译器,是一个根据源代码生成机器码的程序。 • > g++ main.cpp -o a.out • 该命令会调用编译器程序 g++ ,让他读取 main.cpp 中的字符串(称为源码),并根据 C+ + 标准生成相应的机器指令码,输出到 a.out 这个文件中,(称为可执行文件)。0 码力 | 32 页 | 11.40 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 03 现代 C++ 进阶:模板元编程,后半段主要介绍并行编程与优化。 1.课程安排与开发环境搭建: cmake 与 git 入门 2.现代 C++ 入门:常用 STL 容器, RAII 内存管理 3.现代 C++ 进阶:模板元编程与函数式编程 4.编译器如何自动优化:从汇编角度看 C++ 5.C++11 起的多线程编程:从 mutex 到无锁并行 6.并行编程常用框架: OpenMP 与 Intel TBB 7.被忽视的访存优化:内存带宽与 cpu twice(“hello”) ,从而出错。 • 可能的解决方案: SFINAE 。 模板函数:默认参数类型 • 但是如果模板类型参数 T 没有出现在函数 的参数中,那么编译器就无法推断,就不 得不手动指定了。 • 但是,可以通过 • template • 表示调用者没有指定时, T 默认为 int 。 模板参数:整数也可以作为参数 template 传入的 N ,是一个编译期常量,每个不同的 N ,编译器都会单独生成一份代码,从而可以对他做单独的优化 。 • 而 func(int N) ,则变成运行期常量,编译器无法自动优化,只 能运行时根据被调用参数 N 的不同。 • 比如 show_times<0>() 编译器就可以自动优化为一个空函数。 因此模板元编程对高性能编程很重要。 • 通常来说,模板 0 码力 | 82 页 | 12.15 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 10 从稀疏数据结构到量化数据类型b) % b 做循环边界 ,从而避免负方向上出错。然而这还是避免不了 a < -b 时的出错。 • 正确的写法是: (a % b + b) % b • 如果 b 是常数且为 2 的幂次方,编译器会检测到, 并替换为更高效的位运算,反而减少了计算量。 • 此外如果 b 一定是 2 的幂次方,那么 (unsigned)a % b 也可以(先转换成无符号的取模)。 高效的解决:位运算 & 载了 operator= 和 operator bool 的 std::_Bit_reference 对象,而且效率很低。 • 如果配合用 decltype 和 auto 的话,他们不会正确推导出 bool ,影响我们正常使用模板元编 程。 • 一般认为 vector是 C++ 标准库设计上的一个败笔,是为了向前兼容才保持这样不变的 。 • 他们就不应该直接特化 vector 255 ),然后把刚刚的系数 100 改小到 2 ,成功算对结果了,代价是精度损失了 不少。 • 其实 GPU 存储贴图一般也是用的定点数 uint8_t (范围从 0 到 255 ),着色器在读取的时候才会把他 转换成 float (范围从 0.0 到 1.0 )。这就是浮点数的 量化,存储时转换成低精度的定点数,读取时再转换 回高精度的浮点数,从而节省 4 倍内存带宽,提升 GPU 0 码力 | 102 页 | 9.50 MB | 1 年前3
共 32 条
- 1
- 2
- 3
- 4













