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 缓存机制 4GB 限制外,也有一定性能优势。 8 位, 16 位, 32 位, 64 位版本 al, ax, eax, rax r15b, r15w, r15d, r15 AT&T 汇编语言 GCC 编译器所生成的汇编语言就属于这种 返回值:通过 eax 传出 movl $42, %eax 相当于: eax = 42; 前 6 个参数:分别通过 edi , esi , edx , ecx , r8d0 码力 | 108 页 | 9.47 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 11 现代 CMake 进阶指南
自动调用本地的构建系统在 build 里构建,即: make -C build -j4 // 调用本地的构建系统执行 install 这个目标,即安 装 -D 选项:指定配置变量(又称缓存变量) • 可见 CMake 项目的构建分为两步: • 第一步是 cmake -B build ,称为配置阶段( configure ),这时只检测环境并生成构建规则 • build 目录下生成本地构建系统能识别的项目文件( Makefile 或是 .sln ) • 第二步是 cmake --build build ,称为构建阶段( build ),这时才实际调用编译器来编译代码 • 在配置阶段可以通过 -D 设置缓存变量。第二次配置时,之前的 -D 添加仍然会被保留。 • cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8 第二次配置时没有 -D 参数,但是之前的 -D 设置的变量都会被保留 • (此时缓存里仍有你之前定义的 CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX ) -G 选项:指定要用的生成器 • 众所周知, CMake 是一个跨平台的构建系统,可以从 CMakeLists.txt 生成不同类型的构建系 统(比如 Linux 的 make , Windows 的 MSBuild0 码力 | 166 页 | 6.54 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 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/ 关于作者(再续) • 主导 Zeno 节点仿真框架的开发( https://github.com/zenustech/zeno ) 什么是编译器 • 编译器,是一个根据源代码生成机器码的程序。 • > g++ main.cpp -o a.out • 该命令会调用编译器程序 g++ ,让他读取 main.cpp 中的字符串(称为源码),并根据 C+ + 标准生成相应的机器指令码,输出到 a.out0 码力 | 32 页 | 11.40 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 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 ,都能编 译出来。 • 符号,和性能优化意义上的内联无关。 • 优化意义上的内联指把函数体直接放到调用者那里去。 • 因此 CUDA 编译器提供了一个“私货”关键字: __inline__ 来 声明一个函数为内联。不论是 CPU 函数还是 GPU 都可以使 用,只要你用的 CUDA 编译器。 GCC 编译器相应的私货则 是 __attribute__((“inline”)) 。 • 注意声明为 __inline__ 通常都是一些可以内联的函数,数学计 算表达式之类的,一个个加上太累了,所以产生了这个 需求。 • 不过必须指定 --expt-relaxed-constexpr 这个选项才能 用这个特性,我们可以用 CMake 的生成器表达式来实 现只对 .cu 文件开启此选项(不然给到 gcc 就出错 了)。 • 当然, constexpr 里没办法调用 printf ,也不能用 __syncthreads 之类的0 码力 | 142 页 | 13.52 MB | 1 年前3《深入浅出MFC》2/e
framework 的观念,以及MFC 骨干程序。所谓骨干程序,是指Visual C++ 的工具AppWizard 所产生出来的程序代码。当然,AppWizard 会根据你的选项做出不同 的程序代码,我所据以解说的,是大众化选项下的产品。 第四篇以微软公司附于Visual C++ 光盘片上的一个范例程序Scribble 为主轴,一步一步加 上新的功能。并在其间深入介绍Runtime Type 的软件开发工具,但现在已经变成一个一般性名词。凡以 Windows raw API 撰写的程序我们通常也称为SDK 程序。也有人把Windows API 称为 SDK API。Borland 公司的C++ 编译器也支持相同的SDK API(那当然,因为Windows 只有一套)。本书如果出现「SDK 程序」这样的名词,指的就是以Windows raw API 完 成的程序。 MFC - Microsoft 上顺利编译。原因出在VC++ 4.2 和VC++ 5.0 似乎未能支持"forward declaration of data structure class"(但是我怀疑VC++ 怎么会走退步?是不是有什么选项 可以设定)。无论如何,只要将CStroke 的声明搬移到SCRIBBLEDOC.H 的最前面, 然后再接续CScribbleDoc 的声明,即可顺利编译。请阅读本书第8章「CScribbleDoc0 码力 | 1009 页 | 11.08 MB | 1 年前3现代C++ 教程:高速上手C++11/14/17/20
本书每章最后还加入了少量难度极小的习题,仅用于检验你是否能混合运用当前章节中的知识点。你 可以在这里找到习题的答案,文件夹名称为章节序号。 第 1 章迈向现代 C++ 编译环境:本书将使用 clang++ 作为唯一使用的编译器,同时总是在代码中使用 -std=c++2a 编译 标志。 > clang++ -v Apple LLVM version 10.0.1 (clang-1001.0.46.4) Target: Stroustrup • C++ 历史 • C++ 特性在 GCC/Clang 等编译器中的支持情况 • C++98 与 C99 之间的区别 11 第 2 章语言可用性的强化 第 2 章语言可用性的强化 当我们声明、定义一个变量或者常量,对代码进行流程控制、面向对象的功能、模板编程等这些都 是运行时之前,可能发生在编写代码或编译器编译代码时的行为。为此,我们通常谈及语言可用性,是 指那些发生在运行时之前的语言行为。 出现的目的是为了替代 NULL。在某种意义上来说,传统 C++ 会把 NULL、0 视为同一种东 西,这取决于编译器如何定义 NULL,有些编译器会将 NULL 定义为 ((void*)0),有些则会直接将其定义 为 0。 C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void*)0), 那么在下面这句代码中: char *ch = NULL;0 码力 | 83 页 | 2.42 MB | 1 年前3C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串
这些整数,而 8 位整数的表示范围是 2^8 也就是 0~255 ,足以表示所有 ASCII 字符了(多余的部分实际上被用于表示 中文)。 • char 和整数无异,例如 ‘ a’ 实际上会被编译器翻译成他对应的 ASCII 码: 97 。写 ‘ a’ 和写 (char)97 是完全一样的,方便阅读的语法糖而已。 “char 即整数”思想应用举例 “char 即整数”思想应用举例 C 语言帮手函数 语言其实只规定了 unsigned char 是无符号 8 位整数, signed char 是有 符号 8 位整数,而 char 类型只需是 8 位整数即可,可以是有符号也可以 是无符号,任凭编译器决定( C 标准委员会传统异能, khronos 直呼内行) 。 • 以 GCC 为例,他规定 char 在 x86 架构是有符号的 (char = signed char) ,而在 arm 架构上则认为是无符号的 • 而 \ 比较厉害,他是编译器内部专门为他“开了个后门”。 • 编译器检测到字符串中出现 \ 就会把下一个字符特殊处理。 • 而 % ,编译器并不会特殊处理 % ,是 printf 函数内部在运行时处理了 % 的下一个字符。 • % 就像你和同学随手“拉钩”定下的约定,这是 printf 约定俗成的。 • \ 就像正式合同,有法律效力的,这是 C 语言编译器规定好的。 C++ 字符串类0 码力 | 162 页 | 40.20 MB | 1 年前3Hello 算法 1.1.0 C++ 版
元,则收银员需要找我们 31 元。他 会很自然地完成如图 1‑3 所示的思考。 1. 可选项是比 31 元面值更小的货币,包括 1 元、5 元、10 元、20 元。 2. 从可选项中拿出最大的 20 元,剩余 31 − 20 = 11 元。 3. 从剩余可选项中拿出最大的 10 元,剩余 11 − 10 = 1 元。 4. 从剩余可选项中拿出最大的 1 元,剩余 1 − 1 = 0 元。 5. 完成找零,方案为 。 图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为尾递归(tail recursion)。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 Tip 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即 使函数是尾递归形式,仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思0 码力 | 379 页 | 18.47 MB | 1 年前3Hello 算法 1.0.0b5 C++版
元,则收银员需要找我们 31 元。他 会很自然地完成如图 1‑3 所示的思考。 1. 可选项是比 31 元面值更小的货币,包括 1 元、5 元、10 元、20 元。 2. 从可选项中拿出最大的 20 元,剩余 31 − 20 = 11 元。 3. 从剩余可选项中拿出最大的 10 元,剩余 11 − 10 = 1 元。 4. 从剩余可选项中拿出最大的 1 元,剩余 1 − 1 = 0 元。 5. 完成找零,方案为 。 图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出报错。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化,因此即使函数 是尾递归形式,但仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的0 码力 | 377 页 | 30.69 MB | 1 年前3Hello 算法 1.0.0 C++版
元,则收银员需要找我们 31 元。他 会很自然地完成如图 1‑3 所示的思考。 1. 可选项是比 31 元面值更小的货币,包括 1 元、5 元、10 元、20 元。 2. 从可选项中拿出最大的 20 元,剩余 31 − 20 = 11 元。 3. 从剩余可选项中拿出最大的 10 元,剩余 11 − 10 = 1 元。 4. 从剩余可选项中拿出最大的 1 元,剩余 1 − 1 = 0 元。 5. 完成找零,方案为 。 图 2‑4 递归调用深度 在实际中,编程语言允许的递归深度通常是有限的,过深的递归可能导致栈溢出错误。 2. 尾递归 有趣的是,如果函数在返回前的最后一步才进行递归调用,则该函数可以被编译器或解释器优化,使其在空 间效率上与迭代相当。这种情况被称为「尾递归 tail recursion」。 ‧ 普通递归:当函数返回到上一层级的函数后,需要继续执行代码,因此系统需要保存上一层调用的上下 普通递归:求和操作是在“归”的过程中执行的,每层返回后都要再执行一次求和操作。 ‧ 尾递归:求和操作是在“递”的过程中执行的,“归”的过程只需层层返回。 图 2‑5 尾递归过程 � 请注意,许多编译器或解释器并不支持尾递归优化。例如,Python 默认不支持尾递归优化, 因此即使函数是尾递归形式,仍然可能会遇到栈溢出问题。 3. 递归树 当处理与“分治”相关的算法问题时,递归往往比迭代的思0 码力 | 378 页 | 17.59 MB | 1 年前3
共 25 条
- 1
- 2
- 3