C++高性能并行编程与优化 - 课件 - Zeno 中的现代 C++ 最佳实践 IObject 一切对象的公共基类。 • INode 一切节点的公共基类。 多态的经典案例 • IObject 具有一个 eatFood 纯虚函数,而 CatObject 和 DogObject 继承自 IObject ,他 们实现了 eatFood 这个虚函数,实现了多态。 • 注意这里解构函数( ~IObject )也需要是虚函数 ,否则以 IObject * 存储的指针在 delete 时只 这里的解构函数也是多态的,他根据类型的不同 调用不同派生类的解构函数。 多态用于设计模式之“模板模式” • 这样之后如果有一个任务是要基于 eatFood 做文章,比如要重复 eatFood 两遍。 • 就可以封装到一个函数 eatTwice 里,这个函数只需接受他们共同的基类 IObject 作为参数,然后调 用 eatFood 这个虚函数来做事(而不是直接操作具体的猫和狗本身)。 • 这样只需要写一遍 思考:能不能把拷贝构造函数也作为虚函数? • 现在我们的需求有变,不是去对同一个对象调用两次 eatTwice ,而是先把对象复制一份 拷贝,然后对对象本身和他的拷贝都调用一次 eatFood 虚函数。 • 代码如下,这要怎么个封装法呢?你可能会想,是不是可以把拷贝构造函数也声明为虚函 数,这样就能实现了拷贝的多态?不行,因为 C++ 规定“构造函数不能是虚函数”。 模板函数?未免有些差强人意 • 索性把 eatTwice0 码力 | 54 页 | 3.94 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 02 现代 C++ 入门:RAII 内存管理基于 C++17 标准,有时会谈到 C++20 作为扩展阅读。 C++ 有哪些面向对象思想? C++ 思想:封装 比如要表达一个数组,需要:起始地址指针 v ,数组大小 nv 将多个逻辑上相关的变量包装成一个类 因此 C++ 的 vector 将他俩打包起来,避免程序员犯错 封装:不变性 比如当我要设置数组大小为 4 时,不能只 nv = 4 还要重新分配数组内存,从而修改数组起始地址 v 这种情况出现时,就意味着你需要把成员变量的读写封装为成员函数 不变性:请勿滥用封装 • 仅当出现“修改一个成员时,其他也成员要 被修改,否则出错”的现象时,才需要 getter/setter 封装。 • 各个成员之间相互正交,比如数学矢量类 Vec3 ,就没必要去搞封装,只会让程序员 变得痛苦,同时还有一定性能损失:特别 是如果 getter/setter 函数分离了声明和定 义,实现在另一个文件时! C++ << 取决于内存的随机值 编译器默认生成的构造函数:无参数( POD 陷阱解决方案) • 不过我们可以手动指定初始化 weight 为 0 。 • 通过 {} 语法指定的初始化值,会在编译器自 动生成的构造函数里执行。 编译器默认生成的构造函数:无参数( POD 陷阱解决方案,续) • 不过我们可以手动指定初始化 weight 为 0 。 • 通过 {} 语法指定的初始化值,不仅会在编译0 码力 | 96 页 | 16.28 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 11 现代 CMake 进阶指南项目的构建分为两步: • 第一步是 cmake -B build ,称为配置阶段( configure ),这时只检测环境并生成构建规则 • 会在 build 目录下生成本地构建系统能识别的项目文件( Makefile 或是 .sln ) • 第二步是 cmake --build build ,称为构建阶段( build ),这时才实际调用编译器来编译代码 • 在配置阶段可以通过 -D 设置缓存变量。第二次配置时,之前的 -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0 • ↑ 设置安装路径为 /opt/openvdb-8.0 (会安装到 /opt/openvdb-8.0/lib/libopenvdb.so ) • cmake -B build -DCMAKE_BUILD_TYPE=Release • ↑ 设置构建模式为发布模式(开启全部优化) • cmake -B build ← 第二次配置时没有 现在只需要写一次 CMakeLists.txt ,他会视不同的操作系统,生成不同构建系统的规则文件。 • 那个和操作系统绑定的构建系统( make 、 MSBuild )称为本地构建系统( native buildsystem )。 • 负责从 CMakeLists.txt 生成本地构建系统构建规则文件的,称为生成器( generator )。 -G 选项:指定要用的生成器 • Linux0 码力 | 166 页 | 6.54 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串就像你和同学随手“拉钩”定下的约定,这是 printf 约定俗成的。 • \ 就像正式合同,有法律效力的,这是 C 语言编译器规定好的。 C++ 字符串类 第 3 章 C 语言字符串操作繁琐 封装的 std::string 应运而生 封装的 std::string 应运而生 • string 可以从 const char * 隐式构造: • string s = “hello”; • string 具有 + 、 类型的,他们没有 + 运算 符。 • C++ 为了向前兼容,没办法改变 C 语言的这项规定,只能退而求其次,他另 外定义了一个 string 类,重载了 + 运算符,并告诉同学们:以后尽量用我这 个封装好的类,不要直接用 C 语言的 const char * 。 • 因此如果需要把两个字符串加在一起,就必须至少有一方是 string 才行。 • 可以用 string(“hello”) 这种形式包裹住每个字符串常量,这样就方便用 正确: C++14 新特性:自定义字面量后缀 • 不少同学就觉得这样好麻烦,其他语言都是直接 “ hello” 就是字符串类 型, C++ 还得套一层壳 string(“hello”) 才能变成安全封装的类型,才能用他 的成员函数。 • 因此, C++14 引入了一项缓解“键盘压力”的新特性: • 写 “ hello”_s 就相当于写 operator“”_s(“hello”, 5) ,就相当于0 码力 | 162 页 | 40.20 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 12 从计算机组成原理看 C 语言指针int 甚至可以是 16 位的!只不过主流操作系统一致认为是 32 位的而已,并不是标准所保 证的。 • 为了解决不同操作系统上对类型定义混乱的问题, C 语言标准引入了 stdint.h 这个头文件 。 • 他里面包含一系列类型别名 (typedef) ,这些别名保证不论是什么操作系统什么架构,都是 固定的大小,例如: • typedef char int8_t; • typedef 传一个空指针,就表示“用户不想指定 这个参数”的意思。 C++ 可以用更安全的 func(std::optionalpars) 来替代。 NULL 的定义为什么是这样的? • 如果你看过标准库的头文件内容,会看到 NULL 的 本质无非是一个宏。那为什么要这样定义呢? • 可见他在 C++ 中会直接定义为常数 0 ,而 C 语言 中却定义为 ((void*)0) ,为什么会区别对待? • 提供了符合 RAII 思想的 vector 容器,他会在自己解构时自动释放内存。 • 小白程序员只需要保证自己总是在用 C++ 封 装好的容器而不是 C 语言指针,就可以基本 保证没有内存泄露。但是封装的太好了也有效 率问题,因此追求极致性能时还是会直接操作 原始的 C 语言指针。此外如果不理解容器的 底层原理也很容易犯错,因此我们还是要好好 学习 C 语言思想的。 C 语言特性:函数声明为 0 码力 | 128 页 | 2.95 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 13 C++ STL 容器全解之 vector迭代器入门 迭代器模式 • 如果要把右边这个打印的操作封装起来, 该怎么做? 迭代器模式 • 如果要把右边这个打印的操作封装起来, 该怎么做? • 可以用一个函数来封装打印操作: • print(vectorconst &a); 迭代器模式 • 如果要把右边这个打印的操作封装起来, 该怎么做? • 可以用一个函数来封装打印操作: • print(vector ++ 的前置和后置 • 迭代器的自增运算符分为 ++p 和 p++ 两种写法。 • 他们都会产生 p = p + 1 的效果,但是有一个细微的 区别,就是他们被作为表达式时的返回值。 • ++p 会返回自增后的值 p + 1 ,这和 p += 1 完全一 样,同样因为返回的是一个左值引用所以还可以继续 自增比如 ++++p 。 • p++ 会返回自增前的值 p ,但是执行完以后 p 却又 + 1 了,非常迷惑) • 正因如此,后置自增需要先保存旧的迭代器,然后自 增自己,再返回旧迭代器,可能会比较低效。 • 在 C++ 中我推荐尽可能地多用前置自增 ++p 。 在运算符重载上,沙雕的 C++ 标准 委员会规定, operator++(int) 这个重 载是后置自增 p++ ,不带任何参数的 operator++() 这个重载是前置自增, 之所以这样是因为同名函数只能通过 参数列表类型来区分,这个 0 码力 | 90 页 | 4.93 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 05 C++11 开始的多线程编程,表示让当 前线程休眠直到某个时间点。 第 1 章:线程 进程与线程 • 进程是一个应用程序被操作系统拉起来加载到内存之后从开始执行到执行结束的这样一个 过程。简单来说,进程是程序(应用程序,可执行文件)的一次执行。比如双击打开一个 桌面应用软件就是开启了一个进程。 • 线程是进程中的一个实体,是被系统独立分配和调度的基本单位。也有说,线程是 CPU 可 执行调度的最小单位。也就是说,进程本身并不能获取 • 我们的程序常常需要同时处理多个任务。 • 例如:后台在执行一个很耗时的任务,比 如下载一个文件,同时还要和用户交互。 • 这在 GUI 应用程序中很常见,比如浏览 器在后台下载文件的同时,用户仍然可以 用鼠标操作其 UI 界面。 没有多线程:程序未响应 • 没有多线程的话,就必须等文件下载完了 才能继续和用户交互。 • 下载完成前,整个界面都会处于“未响应”状 态,用户想做别的事情就做不了。 这样就可以一边和用户交互,一边在另一 个线程里慢吞吞下载文件了。 错误:找不到符号 pthread_create • 但当我们直接尝试编译刚才的代码,却在链接时发生了错误。 • 原来 std::thread 的实现背后是基于 pthread 的。 • 解决: CMakeLists.txt 里链接 Threads::Threads 即可: 有了多线程:异步处理请求 • 有了多线程的话,文件下载和用户交互分 别在两个线程,同时独立运行。从而下载0 码力 | 79 页 | 14.11 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 08 CUDA 开启的 GPU 编程里直接加你 的 .cu 文件,和 .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 的一大好处, CUDA 和 C++ 的关 系就像 C++ 和 C 的关系一样,大部分都兼容 ,因此能很方便地重用 C++ 现有的任何代码库 ,引用 C++ 头文件等。 • host 代码和 device 代码写在同一个文件内,这 是 OpenCL 做不到的。 编写一段在 GPU 上运行的代码 • 定义函数 kernel ,前面加上 __global__ 修 饰符,即可让他在 GPU 算表达式之类的,一个个加上太累了,所以产生了这个 需求。 • 不过必须指定 --expt-relaxed-constexpr 这个选项才能 用这个特性,我们可以用 CMake 的生成器表达式来实 现只对 .cu 文件开启此选项(不然给到 gcc 就出错 了)。 • 当然, constexpr 里没办法调用 printf ,也不能用 __syncthreads 之类的 GPU 特有的函数,因此也不能 完全替代0 码力 | 142 页 | 13.52 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 10 从稀疏数据结构到量化数据类型结构。 • 下面这个例子中的稀疏数据结构,用这种语言可以表示为 hash().pointer(11).dense(8) 。 封装起来,方便多层解耦 封装起来,方便多层解耦 封装起来,方便多层解耦 封装起来,方便多层解耦 封装起来,方便多层解耦 • 这样就封装好了,通过模板的方式实现了 自定义的稀疏数据结构: • hash().pointer(11).dense(8) 开源的体素处理库: glibc ,直接和操作系统打交道。 解决:绕开 glibc ,直接调用操作系统的 mmap 分配 • 那么就要用到 Unix 类操作系统的 mmap 函数,他位于 sys/mman.h 头文件里。 • Windows 可以用 VirtualAllocateEx 之类。 • mmap 出来的起始地址保证是对齐到 4KB 的,读写访问其 中偏移地址时,会按页的粒度自动分配和释放内存,从而满0 码力 | 102 页 | 9.50 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 07 深入浅出访存优化解决办法就是,我们也用 stream 指令, 这样就可以和标准库优化过的 memset 一 样快了。 Intel Intrinsics Guide • _mm 系列指令出自头文件。 • 指令的文档可以看这个网站: • https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html • 里 效利用内存 ,随机访问和插桩又特别高效。有兴趣可以研究一下他们的论文,也用了莫顿序增强 TLB 和缓存的局域性,非常精彩。 vector 也可以不初始化:只需使用一个帮手类 也可以使用小彭老师封装好的帮手类 重复分配效率低 • 即使第二次分配的是同一段差不多大小的内存,也是会产生缺页中断,花费分配时间的。 • glibc 的 malloc 实现,不会重复利用现有的内存。 改用 tbbmalloc 标准库的 new 和 malloc :只保证 16 字节对齐 • 还有 _mm_malloc(n, aalign) 可以分配对齐 到任意 a 字节的内存。他在 这个头文件里。是 x86 特有的,并且需要通 过 _mm_free 来释放。 • 还有一个跨平台版本(比如用于 arm 架构) 的 aligned_alloc(align, n) ,他也可以分配对 齐到任意 0 码力 | 147 页 | 18.88 MB | 1 年前3
共 39 条
- 1
- 2
- 3
- 4













