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 缓存机制 新增的寄存器,给了汇编程序员更大的空间,降低了编译 器处理寄存器翻车( register spill )的压力。 • 因此 64 位比 32 位机器相比,除了内存突破 4GB 限制外,也有一定性能优势。 8 位, 16 位, 32 位, 64 位版本 al, ax, eax, rax r15b, r15w, r15d, r15 AT&T 汇编语言 GCC 编译器所生成的汇编语言就属于这种 返回值:通过0 码力 | 108 页 | 9.47 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 11 现代 CMake 进阶指南CMake 相比,使用 更方便,功能更强大。 为什么要学习现代 CMake ? 现代 CMake : 古代 CMake : 第 0 章:命令行小技巧 传统的 CMake 软件构建 / 安装方式 • mkdir build • cd build • cmake .. • make -j4 • sudo make install • cd .. • 需要先创建 build 目录 build 目录 • 在 build 目录运行 cmake < 源码目录 > 生成 Makefile • 执行本地的构建系统 make 真正开始构建( 4 进程并 行) • 让本地的构建系统执行安装步骤 • 回到源码目录 现代 CMake 提供了更方便的 -B 和 --build 指令,不同平台,统一命 令! • cmake -B build • cmake --build build build ,称为构建阶段( build ),这时才实际调用编译器来编译代码 • 在配置阶段可以通过 -D 设置缓存变量。第二次配置时,之前的 -D 添加仍然会被保留。 • cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0 • ↑ 设置安装路径为 /opt/openvdb-8.0 (会安装到 /opt/openvdb-8.0/lib/libopenvdb0 码力 | 166 页 | 6.54 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 16 现代 CMake 模块化项目管理指南/biology/include 这个头文件搜 索路径。 五、子项目的源文件 • 这里我们给 biology 批量添加了 src/*.cpp 下的全部源码文 件。 • 明明只有 *.cpp 需要编译,为什么还添加了 include/*.h ? 为了头文件也能被纳入 VS 的项目资源浏览器,方便编辑。 • 因为子项目的 CMakeLists.txt 里指定的路径都是相对路径 ,所以这里指定 那么这个头文件是不需要导入 Animal.h 的,只需要一个前置声明 struct Animal ,只有实际调用了 Animal 成员函数的源文件需要导入 Animal.h 。 • 好处:加快编译速度,防止循环引用。 十一、以项目名为名字空间( namsepace ),避免符号冲突 • 在声明和定义外面都套一层名字空间,例如此处我的子项目名是 biology ,那 我就 biology::Animal 中则是基于定义者所在路径,优先访问定义者的作用域。这里需要 set(key val PARENT_SCOPE) 才能修改到外面的变量。 第二章:第三方库 / 依赖项配置 用 find_package 寻找系统中安装的第三方库并链接他们 find_package 命令 • 常用参数列表一览: • find_package([version] [EXACT] [QUIET] [CONFIG] 0 码力 | 56 页 | 6.87 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.被忽视的访存优化:内存带宽与 用户) CMake 3.12 及以上(跨平台作业) Git 2.x (作业上传到 GitHub ) CUDA Toolkit 10.0 以上( GPU 专题) 关于作者 • 我是 Taichi 编译器的贡献者之一( https://github.com/taichi-dev/taichi ) 关于作者(续) • 我是 Taichi Blend 的作者( https://github.com 关于作者(再续) • 主导 Zeno 节点仿真框架的开发( https://github.com/zenustech/zeno ) 什么是编译器 • 编译器,是一个根据源代码生成机器码的程序。 • > g++ main.cpp -o a.out • 该命令会调用编译器程序 g++ ,让他读取 main.cpp 中的字符串(称为源码),并根据 C+ + 标准生成相应的机器指令码,输出到 a.out0 码力 | 32 页 | 11.40 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 08 CUDA 开启的 GPU 编程文件,和 .cpp 一样。 https://www.nvidia.cn/docs/IO/51635/NVIDIA_CUDA_Programming_Guide_1.1_chs.pdf CUDA 编译器兼容 C++17 • CUDA 的语法,基本完全兼容 C++ 。包括 C+ +17 新特性,都可以用。甚至可以把任何一个 C++ 项目的文件后缀名全部改成 .cu ,都能编 译出来。 • 运行以后,就会在 GPU 上执行 printf 了。 • 这里的 kernel 函数在 GPU 上执行,称为核 函数,用 __global__ 修饰的就是核函数。 没有反应?同步一下! • 然而如果直接编译运行刚刚那段代码,是不会打印出 Hello, world! 的。 • 这是因为 GPU 和 CPU 之间的通信,为了高效,是异 步的。也就是 CPU 调用 kernel<<<1, 1>>>() 符号,和性能优化意义上的内联无关。 • 优化意义上的内联指把函数体直接放到调用者那里去。 • 因此 CUDA 编译器提供了一个“私货”关键字: __inline__ 来 声明一个函数为内联。不论是 CPU 函数还是 GPU 都可以使 用,只要你用的 CUDA 编译器。 GCC 编译器相应的私货则 是 __attribute__((“inline”)) 。 • 注意声明为 __inline__0 码力 | 142 页 | 13.52 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 06 TBB 开启的并行编程之旅,后半段主要介绍并行编程与优化。 1.课程安排与开发环境搭建: cmake 与 git 入门 2.现代 C++ 入门:常用 STL 容器, RAII 内存管理 3.现代 C++ 进阶:模板元编程与函数式编程 4.编译器如何自动优化:从汇编角度看 C++ 5.C++11 起的多线程编程:从 mutex 到无锁并行 6.并行编程常用框架: OpenMP 与 Intel TBB 7.被忽视的访存优化:内存带宽与 使是单核,多线程对我也有用。 因特尔开源的并行编程库: TBB https://link.springer.com/chapter/10.1007%2F978-1-4842-4398-5_2 安装 TBB • Ubuntu: • sudo apt-get install libtbb-dev • Arch Linux: • sudo pacman -S tbb • Windows: Windows: • .\vcpkg install tbb:x64-windows • Mac OS: • .\vcpkg install tbb:x64-macos • Other: • 从源码构建安装,参考: https://blog.csdn.net/weixin_42973508/article/details/111681426 详见 https://www.bilibili.com/video/BV1fa411r7zp0 码力 | 116 页 | 15.85 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.被忽视的访存优化:内存带宽与 ,而是会去调用模板函数 twice(“hello”) ,从而出错。 • 可能的解决方案: SFINAE 。 模板函数:默认参数类型 • 但是如果模板类型参数 T 没有出现在函数 的参数中,那么编译器就无法推断,就不 得不手动指定了。 • 但是,可以通过 • template • 表示调用者没有指定时, T 默认为 int 。 模板参数:整数也可以作为参数 func(vector t) 这样则可以限定仅仅 为 vector 类型的参数。 • 这里用了 const & 避免不必要的的拷贝。 • 不过,这种部分特化也不支持隐式转换。 为什么要支持整数作为模板参数:因为是编译期常量 • 你可能会想,模板只需要支持 class T 不就行了?反正 int N 可以作为函数的 参数传入,模板还不支持浮点。 • template void func(); 0 码力 | 82 页 | 12.15 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 02 现代 C++ 入门:RAII 内存管理,后半段主要介绍并行编程与优化。 1.课程安排与开发环境搭建: cmake 与 git 入门 2.现代 C++ 入门:常用 STL 容器, RAII 内存管理 3.现代 C++ 进阶:模板元编程与函数式编程 4.编译器如何自动优化:从汇编角度看 C++ 5.C++11 起的多线程编程:从 mutex 到无锁并行 6.并行编程常用框架: OpenMP 与 Intel TBB 7.被忽视的访存优化:内存带宽与 自定义构造函数:单个参数 自定义构造函数:单个参数(陷阱) 自定义构造函数:单个参数(避免陷阱) 避免陷阱体现在哪里? • 加了 explicit 表示必须用 () 强制转换。 • 否则 show(80) 也能编译通过! • 所以,如果你不希望这种隐式转换, • 请给单参数的构造函数加上 explicit 。 • 比如 std::vector 的构造函数 vector(size_t n) 也是 explicit static_cast(ptr) 的错误 。 • 虽然作者也经常会忍不住在 zeno 中用 编译器默认生成的构造函数:无参数(小心 POD 陷阱!) • 除了我们自定义的构造函数外,编译器还会自动生成一些构造函数。 • 当一个类没有定义任何构造函数,且所有成员都有无参构造函数时,编译器会自动生成一 个无参构造函数 Pig() ,他会调用每个成员的无参构造函数。 • 但是请注意,这些类型不会被初始化为 0 码力 | 96 页 | 16.28 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 性能优化之无分支编程 Branchless ProgrammingL3: 编译器是如何优化 if-else 的 xor eax, eax :相当于 mov eax, 0 更高效的写法 test edi, edi :相当于 cmp edi, 0 更高效的写法 setle al :如果上一次 cmp 的结果为小于等于,则把 al 设为 1 ,否则设为 0 add eax, 1 :给 eax 加 1 ,也就是 eax=eax+1 的意思 归纳得出编译器对 if-else 指令检测是否支持,如果在不支持 cmov 的 CPU 上使用会产生 SIGILL 错误。不过现在 64 位的 x86 CPU 都保证自带了 cmov 和 sse 拓展,所以不需要手动 开启什么开关编译器就会自动生成利用 cmov 和 sse 指令的高效代码,这也是 x86-64 的优点之一。 https://www.felixcloutier.com/x86/cmovcc https://www 还有一种“摆烂”的做法: • (cond ? a : b) // 方法 3 • 三目运算符通常会变成和 if-else 一样的分 支,同样会生成条件跳转指令,理应一样 低效。但是有时候编译器会检测到,可以 帮你自动优化成无分支版本的。 “ 妙用加减乘”进行无分支优化的通用公式 • 我比较喜欢方法 2 ,因为他可以很直观地同样适用于多个分支的情况,例如: • if (x < 0)0 码力 | 47 页 | 8.45 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 12 从计算机组成原理看 C 语言指针这个函数取出一个整数的绝对值 。 abs 函数:取出绝对值 • 如果用来获取 float 类型的绝对值呢? • 编译通过了,但是结果却不对! • 你会发现 x 无论如何变化,都是 0.0 。 • 这其实是两个 bug 共同作用的结果。 printf 的错误:不会编译时检测参数类型是否正确 • 第一个 bug 是, printf 其实不知道他的参数是什 么类型,他只看到你字符串里写的 ((void*)0) ,为什么会区别对待? • 这是因为 C++ 规定 0 可以代表空指针,为什么要 这样规定?这样不是很容易出错吗? • 因为如果用 (void*)0 表示空指针,会让下面这个语 句编译出错: int* p = (void*)0 • 因为 C++ 的语法比 C 语言严格,他不允许 void 指针转换成 int ,因此老的定义是不能用的,所以才 额外规定 0 才是空指针。 用 开始数数符合计算机和程序员的习惯。 • 一个大小为 n 的数组,下标从 0 到 n-1 。 实验: char 类型数组 • 当后面有 {} 初始化时, [] 里的 4 可以省 略。 • 这时,编译器会自动推断出数组的大小。 实验: char 类型数组 • 如果没有初始化,而是稍后再赋值的话, 那么 [] 里的 4 就不可省略。 指向数组其中一个元素的指针 • 我们说过,变量的地址(也就是指针)可以通0 码力 | 128 页 | 2.95 MB | 1 年前3
共 26 条
- 1
- 2
- 3













