C++高性能并行编程与优化 - 课件 - 14 C++ 标准库系列课 - 你所不知道的 set 容器int 来 说就是数值的大小比较。那么对 字符串类型 string 要怎么排序 呢? • 其实 string 类定义了运算符重 载 < ,他会按字典序比较两个 字符串。所谓字典序就是优先比 较两者第一个字符(按 ASCII 码比较),如果相等则继续比较 下一个,不相等则直接以这个比 较的结果返回。如果比到末尾都 相等且字符串长度一样,则视为 相等。 警告:千万别用 set做字符串集合。 做字符串集合。 这样只会按字符串指针的地址去判断相等, 而不是所指向字符串的内容。 set 的排序:自定义排序函数 • set 作为模板类,其实有两 个模板参数: set • 第一个 T 是容器内元素的类 型,例如 int 或 string 等。 • 第二个 CompT 定义了你想 要的比较函子, set 内部会 调用这个函数来决定怎么排 序。 • 如果 CompT 不指定,默认 这里我们定义个 MyComp 作为比较函子,和默认的一 样用 < 来比较,所以没有变 化。 set 的排序:自定义排序函数 • 恶搞一下,这里我们把比较 函子 MyComp 定义成只比 较字符串第一个字符 a[0] < b[0] 。 • 神奇的一幕发生了,“ any” 不见了!为什么?因为去重 ! • 为什么 set 会把 “ arch” 和 “ any” 视为相等的元素?明 明内容都不一样? 0 码力 | 83 页 | 10.23 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 12 从计算机组成原理看 C 语言指针类型的绝对值呢? • 编译通过了,但是结果却不对! • 你会发现 x 无论如何变化,都是 0.0 。 • 这其实是两个 bug 共同作用的结果。 printf 的错误:不会编译时检测参数类型是否正确 • 第一个 bug 是, printf 其实不知道他的参数是什 么类型,他只看到你字符串里写的 “ %f” ,会误以 为输入的是 float 参数。 • 如果你输入的是 3 这样的 int 类型常量, float 类型的指针是 float* 。 能够指向一个变量的指针究竟是什么? 地址 字节 指针 p 的内容实际上就是一个整数 4 ,也就是变量 x 中第一个字节的门牌号。 因为 int 类型的四个字节都是紧挨着,所以只需要知道第一个字节的地址就行了。 这样等会通过 * 运算符访问的时候,就可以访问从门牌号 4 开始的一连串四个字节组成的 int 。 注意这里的指针 p 只有四字节,这是 位系统,指针 p 将会是八字节的。 指针的本质是内存地址 • 可见,指针无非是一个 64 位整数,在 32 位计算机上则是个 32 位整数。 • 这个整数表示的是指针所指向变量在内存中 的起始地址(第一个字节所在的门牌号)。 • 我们甚至可以把 int* 强制转换成 unsigned long 类型,来打印出这个门牌号的整数值: 甚至可以有指向指针的指针:二级指针 • 如果 int* 是0 码力 | 128 页 | 2.95 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 13 C++ STL 容器全解之 vector) 侯捷 STL 侯捷 STL vector 容器 vector 容器:构造函数 • vector 的功能是长度可变的数组,他里面的数据 存储在堆上。 • vector 是一个模板类,第一个模板参数是数组里 元素的类型。 • 例如,声明一个元素是 int 类型的动态数组 a : • vectora; vector 容器:构造函数和 size • vector 可以在构造时指定初始长度。 size() const noexcept; vector 容器: operator[] • 要访问 vector 里的元素,只需用 [] 运算符 : • 例如 a[0] 访问第 0 个元素(人类的第一 个) • 例如 a[1] 访问第 1 个元素(人类的第二 个) • int &operator[](size_t i) noexcept; • int const &operator[](size_t 部分即可)。 迭代器模式 • 使用指针和长度做接口的好处是,可以通 过给指针加减运算,选择其中一部分连续 的元素来打印,而不一定全部打印出来。 • 比如这里我们选择打印后三个元素(去掉 了第一个元素,但不必用 erase 修改数组 ,只要传参数的时候同时修改指针和长度 部分即可)。 迭代器模式 • 首地址指针和数组长度看起来不太对称。 • print(char const *begptr 0 码力 | 90 页 | 4.93 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串{‘h’, ‘e’, ‘l’, ‘l’, ‘o’, 0} 。 • hello 每个字符都连续地排列在这个数组中,那么末尾的 0 是怎么回事?原来 C 语言的字符串因为只保留数组的 首地址指针(指向第一个字符的指针),在以 char * 类型 传递给其他函数时,其数组的长度无法知晓。为了确切知 道数组在什么地方结束,规定用 ASCII 码中的“空字符”也 就是 0 来表示数组的结尾。这样只需要一个首地址指针就 寻找子字符串 • find(‘c’) 会在字符串中查找字符 ‘ c’ ,如果找到,返回这个字符第 一次出现所在的位置。如果找不到,返回 -1 。 • 注意:如果原字符串中 ‘ c’ 出现了多次,则只会返回第一个出现的 位置。例如 “ icatchthecat”.find(‘c’) 会返回 1 ,因为他找到的是第 二个字符 ‘ c’ ,而计算机数数从 0 开始,所以他认为是第 1 个没 毛病。 • find(‘c’ c’ ,不同的是他会从第 pos 个字符开始,例如 “ icatchthecat”.find(‘c’, 3) 会返回 4 ,因为是从 第 3 个字符 ‘ t’ 开始查找(人类看来是第四个),所以第一个 ‘ c’ 被略过。 • 如果 pos 所在的位置刚好就是 ‘ c’ ,那么会返回 pos ,例如 “ icatchthecat”.find(‘c’, 4) 会返回 4 。 find 寻找子字符串0 码力 | 162 页 | 40.20 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - Zeno 中的现代 C++ 最佳实践 dataflow-programming 。 节点在 UI 中的表现 节点在 UI 中的表现 节点在 UI 中的表现 节点在 UI 中的表现 main 函数第一个执行? • 众所周知, main 函数是 C/C++ 程序中 第一个执行的函数,是程序的入口点。 • 但,他真的是第一个执行的吗? 全局变量初始化的妙用 • 我们可以定义一个 int 类型全局变量 helper ,然后他的右边其实是可以写一个表达 static 变量也可以指定初始 化表达式,这个表达式会在第一次进入函 数时执行。注意:是第一次进入的时候执 行而不是单纯的在 main 函数之前执行哦 ! 如果函数体内的 static 变量是一个类呢? • 如果函数体内的 static 变量,是一个带有构造函 数和解构函数的类,则 C++ 标准保证: 1. 构造函数会在第一次进入函数的时候调用。 2. 解构函数依然会在 main C++11 起)。 • 这就是函数静态初始化 (func-static-init) 大法。 函数静态初始化可用于“懒汉单例模式” • 如右图。 • getMyClassInstance() 会在第一次调用时创 建 MyClass 对象,并返回指向他的引用。 • 根据 C++ 函数静态变量初始化的规则,之后 的调用不会再重复创建。 • 并且 C++11 也保证了不会多线程的危险, 不需要手动写0 码力 | 54 页 | 3.94 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 07 深入浅出访存优化界的一段内存,真正做到每个块内部不出现跨页现象。 手动预取: _mm_prefetch • 对于不得不随机访问很小一块的情况,还可以通过 _mm_prefetch 指令手动预取一个缓存行。 • 这里第一个参数是要预取的地址(最好对齐到缓存 行),第二个参数 _MM_HINT_T0 代表预取数据 到一级缓存, _MM_HINT_T1 代表只取到二级缓 存, _MM_HINT_T2 代表三级缓存; 里面有详细说明每个指令对应的汇编,方便理解的伪代码,延迟和花费的时钟周期等。 第 4 章:循环合并法 两个循环体 • 原始的代码第一个循环体执行 a[i] = a[i] * 2 ,等乘法全 部结束了以后,再来一个循环体执行 a[i] = a[i] + 1 。 • 因为第一遍循环过了 1GB 的数据,执行到 a[n-1] 时 ,原本 a[0] 处的缓存早已失效,因此第二遍循环开始 读取 a[0] 进亿步优化,出于时间原因就没继续深入, 同学们可以课后研究一下。 第 5 章:内存分配与分页 vector :写入两次,时间都是一样的(理所当然) malloc :写入两次,第一次明显比第二次慢? new int[n] :和 malloc 一样,写入两次,第一次明显比第二次慢? new int[n]{} :后面加个花括号,就和 vector 一样,两次一样快了 结论 • 原理,当调用 malloc 时,操作0 码力 | 147 页 | 18.88 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 06 TBB 开启的并行编程之旅个线程,依次处理 8 个元素的缩并,花了 7 秒 用电量: 1*7=7 度电 总用时: 1*7=7 秒 结论:串行缩并的时间复杂度为 O(n) ,工作复杂度为 O(n) ,其中 n 是元素个数 并行缩并 第一步、 4 个线程,每人处理 2 个元素的缩并,花了 1 秒 第二步、 1 个线程,独自处理 4 个元素的缩并,花了 3 秒 用电量: 4*1+1*3=7 度电 总用时: 1+3=4 秒 结论:并行缩并的时间复杂度为 去 1 个线程,依次处理 8 个元素的扫描,花了 7 秒 用电量: 1*7=7 度电 总用时: 1*7=7 秒 结论:串行扫描的时间复杂度为 O(n) ,工作复杂度为 O(n) 。 并行扫描 第一步、 4 个线程,每人处理 2 个元素的缩并,花了 1 秒 第二步、 1 个线程,独自处理 3 个元素的缩并,花了 3 秒 第三步、 3 个线程,每人处理 2 个元素的缩并,花了 1 秒 用电量: 4*1+1*3+3*1=10 4*1+1*3+3*1=10 度电 总用时: 1+3+1=5 秒 结论:并行扫描的时间复杂度为 O(n/c+c) ,工作复杂度为 O(n+c) ,其中 n 是元素个数 改进的并行扫描( GPU ) 第一步、 4 个线程,每个处理 2 个元素的扫描,花了 1 秒 第而步、 4 个线程,每个处理 2 个元素的扫描,花了 1 秒 第三步、 4 个线程,每个处理 2 个元素的扫描,花了 1 秒 用电量: 3*4=120 码力 | 116 页 | 15.85 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 11 现代 CMake 进阶指南// 调用本地的构建系统执行 install 这个目标,即安 装 -D 选项:指定配置变量(又称缓存变量) • 可见 CMake 项目的构建分为两步: • 第一步是 cmake -B build ,称为配置阶段( configure ),这时只检测环境并生成构建规则 • 会在 build 目录下生成本地构建系统能识别的项目文件( Makefile 或是 cmake_minimum_required 指定最低所需的 CMake 版本 假如你写的 CMakeLists.txt 包含了 3.15 版本才有的特性, 如果用户在老版本上使用,就会出现各种奇怪的错误。 因此最好在第一行加个 cmake_minimum_required(VERSION 3.15) 表示本 CMakeLists.txt 至少需要 CMake 版本 3.15 以上才能运行。 如果用户的 CMake 版本小于 ,则你去找找这个目录: • C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake\ • 你会看到他里面有个 Qt5Config.cmake 对吧。现在,有四种方法让 CMake 找得到他。 • 第一种是设置 CMAKE_MODULE_PATH 变量,添加一下包含 Qt5Config.cmake 这个文 件的目录路径 C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake ,当然刚刚说了尽管你是0 码力 | 166 页 | 6.54 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 17 由浅入深学习 map 容器字符串格式化 8. traits 技术,用户自定义迭代器与算法 9. allocator ,内存管理与对象生命周期 10. C++ 异常处理机制的前世今生 我们都要认真鞋习哦 我们都要认真鞋习哦 第一章:读取与写入 我负责监督你鞋习 ! 我负责监督你鞋习 ! map 查找元素的两个接口 • map 提供了两个查找元素的接口,一曰 [] ,二曰 at 。 • 那么他们两个又有什么区别呢?很多新手都分不清他俩,可能只认识 语法遍历。 迭代器如何遍历 map • 了解了这三点再看 for (auto it = map.begin(); it != map.end(); ++it) 就一目了然了。 • for 里面第一部分,也就是初始化语句: it = map.begin() 代表从最左节点开始出发。 • 第二部分,也就是判断是否退出的条件: it != map.end() 判断是否抵达最右节点的下一个 。 动画演示一 下他的工作原理吧。 1 4 2 8 5 7 内存 地址 a a+1 a+2 a+3 a+4 a+5 vector 查找为什么低效 • 我们要找的数是 5 ,首先从数组第一个元素开始,判断第一个元素是否等于 5 ? 1 == 5 × • 发现不相等,只能继续判断第二个元素是否等于 5 ? 4 == 5 × • 发现不相等,只能继续判断第三个元素是否等于 5 ? 2 == 50 码力 | 90 页 | 8.76 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 性能优化之无分支编程 Branchless Programming出中间结果,先不写回内存),等到了跳转指令(烧开水)处确定了要走分支 A 以后,就 把分支 A 的操作落到实处(写回内存),再把流水线中关于分支 B 的所有指令和数据删了 (浪费了 50% 的算力)。这就是说 CPU 第一次遇见一个分支时,两个分支都会被预执行 。 • 同一段程序被多次执行后,如果每次都是分支 A ,下一次 CPU 就会总结经验,预判到下 一次应该也是分支 A ,并且把 90% 的流水线用于预先执行分支 http://unixwiz.net/techtips/x86-jumps.html 手动进行无分支优化的方法 无分支优化:从汇编角度分析 • 发生了什么?让我们把源码和汇编逐个对应。 • x 是第一个参数(通过 edi 传入,被存入 rbp 指向的堆 栈) • 比较 x 和 0 的大小( cmp 命令把刚存入堆栈的 x 和 0 比较) • 这里 x > 0 返回的是一个 bool 类型(通过指令 得到 NaN ,和原来预期的结果不同。 • 且 sqrt 遇到 x 为负数时会设置 errno 为 EDOM ,产生副作用。而原先的三目运算 符 ?: 由于具有“短路”特性,当 x < 0 时第一个分支 sqrt(x) 不会执行,没有副作用。不一 样了! • 总之,对于这种有副作用的函数,或是有可能返回 NaN 的函数,无法“妙用加减乘”优化 。 冷静分析,学会变通 • return0 码力 | 47 页 | 8.45 MB | 1 年前3
共 24 条
- 1
- 2
- 3













