C++高性能并行编程与优化 - 课件 - 10 从稀疏数据结构到量化数据类型从稀疏数据结构到量化数据类型 by 彭于斌( @archibate ) 往期录播: https://www.bilibili.com/video/BV1fa411r7zp 课程 PPT 和代码: https://github.com/parallel101/course 本课涵盖:稀疏矩阵、 unordered_map 、空间稀 疏网格、位运算、浮点的二进制格式、内存带宽优 化 面向人群:图形学、 >> 3 。 >> 2 = 位运算 >> 对负数的处理 signed 类型的 >> n 会把最高位复制 n 次。 因为补码的特性,这导致负数 >> 的结果仍是负 数。 这样就实现了和 Python 一样的始终向下取整除 法。 >> 2 = unsigned 类型的位运算 >> 不一样 而 unsigned 类型的 >> n 会不会复制最高位, 只是单纯的位移,这会导致负数的符号位单独被位 只是单纯的位移,这会导致负数的符号位单独被位 移,补码失效,造成结果不对。 unsigned 类型的 >> 会生成 shr 指令, signed 类型的 >> 会生成 sar 指令。 我们需要负方向无限延伸的稀疏数据结果,那就只 要 signed 那个就行。 >> 2 = 没有重合时可以用高效的加法:位运算 | • 如果可以保证 a 和 b 满足 a & b = 0 , 如: • 1011000 和 00001100 码力 | 102 页 | 9.50 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 08 CUDA 开启的 GPU 编程如需总的线程编号: blockDim * blockIdx + threadIdx 三维的板块和线程编号 • CUDA 也支持三维的板块和线程区间。 • 只要在三重尖括号内指定的参数改成 dim3 类型即可。 dim3 的构造函数就是接受三 个无符号整数( unsigned int )非常简单 。 • dim3(x, y, z) • 这样在核函数里就可以通过 threadIdx.y 获取 可以直接在核函数里调用核函数并指定参数这么方便…… 不过,这个功能同样需要开启 CUDA_SEPARABLE_COMPILATION 。 第 2 章:内存管理 如何从核函数里返回数据? • 我们试着把 kernel 的返回类型声明为 int ,试 图从 GPU 返回数据到 CPU 。 • 但发现这样做会在编译期出错,为什么? • 刚刚说了 kernel 的调用是异步的,返回的时候 ,并不会实际让 GPU 把核函数执行完毕,必须 等待他执行完毕(和 线程的 join 很像)。所以,不可能从 kernel 里 通过返回值获取 GPU 数据,因为 kernel 返回 时核函数并没有真正在 GPU 上执行。所以核函 数返回类型必须是 void 。 试图解决:通过指针传递 • 那你可能会想,既然不能返回,那作为指 针传入局部变量的引用,不就好了。 • 这样,在 cudaDeviceSynchronize() 以后0 码力 | 142 页 | 13.52 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 01 学 C++ 从 CMake 学起文件越来越多时,一个个调用 g++ 编译链接会变得很麻烦。 • 于是,发明了 make 这个程序,你只需写出不同文件之间的依赖关系,和生成各文件的规则。 • > make a.out • 敲下这个命令,就可以构建出 a.out 这个可执行文件了。 • 和直接用一个脚本写出完整的构建过程相比, make 指明依赖关系的好处: 1. 当更新了 hello.cpp 时只会重新编译 hello.o ,而不需要把 main o 重复写 g++ 命令( %.o: %.cpp )。 • 但坏处也很明显: 1. make 在 Unix 类系统上是通用的,但在 Windows 则不然。 2. 需要准确地指明每个项目之间的依赖关系,有头文件时特别头疼。 3. make 的语法非常简单,不像 shell 或 python 可以做很多判断等。 4. 不同的编译器有不同的 flag 规则,为 g++ 准备的参数可能对 MSVC Windows 则不然。 • 只需要写一份 CMakeLists.txt ,他就能够在调用时生成当前系统所支持的构建系统。 • 需要准确地指明每个项目之间的依赖关系,有头文件时特别头疼。 • CMake 可以自动检测源文件和头文件之间的依赖关系,导出到 Makefile 里。 • make 的语法非常简单,不像 shell 或 python 可以做很多判断等。 • CMake 具有相对高级的语法,内置的函数能够处理0 码力 | 32 页 | 11.40 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 11 现代 CMake 进阶指南CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX ) -G 选项:指定要用的生成器 • 众所周知, CMake 是一个跨平台的构建系统,可以从 CMakeLists.txt 生成不同类型的构建系 统(比如 Linux 的 make , Windows 的 MSBuild ),从而让构建规则可以只写一份,跨平 台使用。 • 过去的软件(例如 TBB )要跨平台,只好 Makefile 放到和源码同一个目录里,我个人的建议是把源码放到 src 目录下 。 第 2 章:项目配置变量 CMAKE_BUILD_TYPE 构建的类型,调试模式还是发布模式 • CMAKE_BUILD_TYPE 是 CMake 中一个特殊的变量,用于控制构建类型,他的值可以 是: • Debug 调试模式,完全不优化,生成调试信息,方便调试程序 • Release 发布模式,优化程度最高,性能最佳,但是编译比 :根项目源码路径(存放 main.cpp 的地方) • CMAKE_BINARY_DIR :根项目输出路径(存放 main.exe 的地方) • PROJECT_IS_TOP_LEVEL : BOOL 类型,表示当前项目是否是(最顶层的)根项目 • PROJECT_NAME :当前项目名 • CMAKE_PROJECT_NAME :根项目的项目名 • 详见: https://cmake.org/0 码力 | 166 页 | 6.54 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串后一次性显示出来的,并不是真的说 Ctrl 就是 ‘ ^’ 这 个字符。 C 语言字符串 第 2 章 C 语言中的字符类型 char • char c = ‘a’; • assert(c == 97); • c = c + 1; • assert(c == ‘b’); • C 语言中规定字符类型为 char 类型,是个 8 位整数。 • 这是因为 ASCII 码只有 0~127 这些整数,而 8 位整数的表示范围是 返回。 帮手函数大全 http://c.biancheng.net/ref/ctype_h/ 关于 char 类型的一个冷知识 • C 语言其实只规定了 unsigned char 是无符号 8 位整数, signed char 是有 符号 8 位整数,而 char 类型只需是 8 位整数即可,可以是有符号也可以 是无符号,任凭编译器决定( C 标准委员会传统异能, khronos 直呼内行) 的指令集处理无符号 8 位整数更高效”,所以擅自把 char 魔改成无 符号的…… • 顺便一提, C++ 标准保证 char , signed char , unsigned char 是三个完全 不同的类型, std::is_same_v 分别判断他们总会得到 false ,无论 x86 还是 arm 。 • 但是奇葩的 C 语言却规定 short , int , long , long long0 码力 | 162 页 | 40.20 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 06 TBB 开启的并行编程之旅parallel_for 基于迭代器区间: parallel_for_each 二维区间上的 for 循环: blocked_range2d 三维区间上的 for 循环: blocked_range3d 所有区间类型 第 2 章:缩并与扫描 缩并( reduce ) 1 个线程,依次处理 8 个元素的缩并,花了 7 秒 用电量: 1*7=7 度电 总用时: 1*7=7 秒 结论:串行缩并的时间复杂度为 O(n) 构建目标的 cmake 项目,有病啊! 你妨碍别人作为子模块用你的项目。没错说的就是你 OpenSim ,张心欣当时浪费好多时间伺候这个沙雕库。 还要指定一个环境变量 SIMBODY_HOME 指向他的依赖项 SimBody 的源码路径,这么 dedicated 让人咋 用? 第 4 章:任务域与嵌套 https://link.springer.com/chapter/10.1007%2F978-1-4842-4398-5_12 begin 和 end 也会返回 tbb::concurrent_vector 的迭代 器类型。 • 第一个 tbb::blocked_range 尖括号里的类 型可以省略是因为 C++17 的 CTAD 特 性。第二个则是用了 decltype 自动推导 ,也可以 (auto r) ,这里写具体类型仅为 教学目的。 TBB 中其他并发容器 第 7 章:并行筛选 筛选( filter0 码力 | 116 页 | 15.85 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - Zeno 中的现代 C++ 最佳实践 https://github.com/zenustech/zeno/tree/zeno2 • Zeno 1.0 所在的分支: https://github.com/zenustech/zeno/ Zeno 中的基本类型 • IObject 一切对象的公共基类。 • INode 一切节点的公共基类。 多态的经典案例 • IObject 具有一个 eatFood 纯虚函数,而 CatObject 和 DogObject IObject * 存储的指针在 delete 时只 会释放 IObject 里的成员,而不会释放 CatObject 里的成员 string m_catFood 。所以 这里的解构函数也是多态的,他根据类型的不同 调用不同派生类的解构函数。 多态用于设计模式之“模板模式” • 这样之后如果有一个任务是要基于 eatFood 做文章,比如要重复 eatFood 两遍。 • 就可以封装到一个函数 eatTwice IOBJECT_DEFINE_CLONE ,其内容是 clone 的实现。这里我们用 std::decay_t快速获取了 this 指针所指向的类型,也就是当前所在类的类型 。 • 宏的缺点是他不遵守命名空间的规则,宏的名 字是全局可见的,不符合 C++ 的高大尚封装思 想。 • 宏: IOBJECT_DEFINE_CLONE • 高大尚 C++ 0 码力 | 54 页 | 3.94 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 12 从计算机组成原理看 C 语言指针• 11111100 表示 252 11111101 表示 253 11111110 表示 254 11111111 表示 255 • 字节实际上就是 C 语言中的 unsigned char 类型。 表示更大范围的整数:字( word ) • 但是单单一个字节表示的范围还是太有限了,只能表示 0 到 255 的值。 • 如何扩大表示范围?简单,用两个字节合在一起即可,例如: • 00000000-00000000 11111111-11111110 表示 65534 11111111-11111111 表示 65535 • 这就是两个字节合成了一个字( word ),实际上就是 C 语言里的 unsigned short 类型 。 不同位数的计算机,字( word )的大小也不一样 • 刚刚说把 2 个字节( byte )拼成一个字( word ),实际上是 16 位计算机的做法。 • 16 位计算机得名就是因为他的字由 位计算机反而是因为 16777216 TB 太大,内存地址被阉割到了 39 位( 512GB )。 • 64 位计算机:小丑竟是我自己 lscpu 命令查看处理器相关信息 C 语言中的整数类型 C 语言的基础整数类型 类型 Unix 32 位 Unix 64 位 Windows 32 位 Windows 64 位 char 8 位 8 位 8 位 8 位 short 16 位 16 位 16 位 160 码力 | 128 页 | 2.95 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 04 从汇编角度看编译器优化的最低位进行运算;也可以是 p 表示矢量 (packed) ,一次对 xmm 中所有位进行运算。 3. 第二个 s 表示单精度浮点数 (single) ,即 float 类型;也可以是 d 表示双精度浮点数 (double) ,即 double 类型。 • addss :一个 float 加法。 • addsd :一个 double 加法。 • addps :四个 float 加法。 • addpd 懂!总之非常高效就对了! 第 5 章:循环 循环中的矢量化:还在伺候指针别名 我们可怜的编译器啊!他还在担心 a 和 b 指向的数组是否有重合。 考虑 func(a, a + 1) 的情况,那样会产生数据依赖链,没法 SIMD 化 。 为了优化而不失正确性,他索性生成两份代码: 一份是 SIMD 的,一份是传统标量的 他在运行时检测 a, b 指针的差是否超过 1024 来判断是否有重叠现 象。 1. 如果没有重叠,则跳转到 double, float (推荐) • 总之,请勿用全局的数学函数,他们是 C 语言 的遗产。始终用 std::sin, std::pow 等。 abs(1.4f) = 1 ,因为 abs 是个参数类型为 int 的函数。 为了避免这种麻烦,请用安全的 std::abs(1.4f) = 1.4f 开启前: sqrt 矢量化失败 开启后: sqrt 矢量化成功 嵌套循环:直接累加,有指针别名问题0 码力 | 108 页 | 9.47 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 07 深入浅出访存优化字节,之 后又没用到剩下的 60 字节,导致浪费了 94% 的带宽。 • 虽说连续、顺序访问是最理想的,然而在使用哈希表等数据结构中,不 可避免的会通过哈希函数得到随机的地址来访问,且 Value 类型可能小 于 64 字节,浪费部分带宽。怎么办? 解决:按 64 字节分块地随机访问 • 解决方法就是,把数据按 64 字节大小分块。随 机访问时,只随机块的位置,而块的内部仍然按 顺序访问。 写入,他能够绕开缓存,将一个 4 字节的写入操 作,挂起到临时队列,等凑满 64 字节后,直接写 入内存,从而完全避免读的带宽。 • 可惜这货只支持 int 做参数,要用 float 还得转换 一下指针类型, bitcast 一下参数。 stream 的特点:不会读到缓存里 • 因为 _mm_stream_si32 会绕开缓存,直 接把数据写到内存,之后读取的话,反而 需要等待 stream 写回执行完成,然后重 _mm_stream_si32 可以一次性写入 4 字 节到挂起队列。而 _mm_stream_ps 可以 一次性写入 16 字节到挂起队列,更加高 效了。 • 他的第二参数是一个 __m128 类型,可以 配合其他手写的 SIMD 指令使用。 • 不过, _mm_stream_ps 写入的地址必须 对齐到 16 字节,否则会产生段错误等异 常。 stream 的限制:最好是连续的写入0 码力 | 147 页 | 18.88 MB | 1 年前3
共 28 条
- 1
- 2
- 3













