C++高性能并行编程与优化 - 课件 - 性能优化之无分支编程 Branchless Programming性能优化 之 无分支编程 Branchless Programming by 彭于斌( @archibate ) 两种代码写法:分支 vs 三目运算符 两种使用方式:排序 vs 不排序 测试结果(均为 gcc -O3 ) 测试结果可视化 图表比较:分支 vs 无分支 分支 无分支 0 0.01 0.02 0.03 耗时(越低越好) 乱序 有序 • 传统的分支方法实现的 或者更满足“对称强迫症”的: • (cond) * a + !(cond) * b // 方法 2 • 还有一种“摆烂”的做法: • (cond ? a : b) // 方法 3 • 三目运算符通常会变成和 if-else 一样的分 支,同样会生成条件跳转指令,理应一样 低效。但是有时候编译器会检测到,可以 帮你自动优化成无分支版本的。 “ 妙用加减乘”进行无分支优化的通用公式 • 为什么采用需要三个分支的 clamp 做测试? • 优化级别在 -O1 以上时,对于只有两个分支的 if-else ,编译器往往会自动检测到可以优化,帮你 应用“妙用加减乘”了,无法体现手动优化的意义。 不同写法的性能测试 • 我们照常编写了测试用例,禁止内联优化,同样生成 10^7 个随机数( -512 到 512 区间)。 • 至于为什么采用需要三个分支的 clamp 做测试?0 码力 | 47 页 | 8.45 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 13 C++ STL 容器全解之 vector比如右边这段代码会得到 4 。 • size_t size() const noexcept; vector 容器: operator[] • 要访问 vector 里的元素,只需用 [] 运算符 : • 例如 a[0] 访问第 0 个元素(人类的第一 个) • 例如 a[1] 访问第 1 个元素(人类的第二 个) • int &operator[](size_t i) noexcept; noexcept; • int const &operator[](size_t i) const noexcept; vector 容器: operator[] • 值得注意的是, [] 运算符在索引超出数组大 小时并不会直接报错,这是为了性能的考虑。 • 如果你不小心用 [] 访问了越界的索引,可能 会覆盖掉别的变量导致程序行为异常,或是访 问到操作系统未映射的区域导致奔溃。 • int 元素全为 0 的数组。 • vector(initializer_listlist); • explicit vector(size_t n); vector 容器 • 添加一个运算符重载用于打印 vector 类 型。 vector 容器:构造函数 • vector 的这个显式构造函数,默认会把所有元 素都初始化为 0 (不必手动去 memset )。 • 如果是其他自定义类,则会调用元素的默认构造 0 码力 | 90 页 | 4.93 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 17 由浅入深学习 map 容器。 • 例如:一个同学问小彭老师在干嘛? • 小彭老师说“我在吃答辩。”那么同学认为这个答辩指的是三体动画,小彭老师在看三体动画。 而不会认为小彭老师真的在吃答辩。 • 小彭老师说“我在拉答辩。”那么同学认为这个答辩指的是答辩(物理),小彭老师在上厕所。 而不会认为小彭老师在制作三体动画。 • 所以这位同学是人类思维,相当于 Python 的精分 API 。而如果另一个同学是硬核的计算 __getitem__ 。 • 也就是说 Python 的 [] 其实是调用了两个不同的运算符重载: • m[key] = val 实际上是 m.__setitem__(key, val) 。 • val = m[key] 实际上是 val = m.__getitem__(key) 。 • C++ 的 [] 就不论读取还是写入都是同一个运算符重载,他只是返回引用,无法区分你是读是写: • value_type 值坑了他。所以他们又另起炉灶,发明了越界时不会自动创建零值,而是能抛出异常的 at 函数。 C++ 和 Python 用法对比 C++ 和 Python 用法对比(运算符重载展开成普通函数后) 简单粗暴的 Java 用法 • 与 Python 和 C++ 不同, Java 放弃了花里胡哨的运算符重载,索性都采用成员函数 get put 来表示,非常明确。主要是为了把 get 和 put 作为接口函数,可以对应多个具体 实现。0 码力 | 90 页 | 8.76 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 15 C++ 系列课:字符与字符串arm 的指令集处理无符号 8 位整数更高效”,所以擅自把 char 魔改成无 符号的…… • 顺便一提, C++ 标准保证 char , signed char , unsigned char 是三个完全 不同的类型, std::is_same_v 分别判断他们总会得到 false ,无论 x86 还是 arm 。 • 但是奇葩的 C 语言却规定 short , int , long , long std::string 应运而生 • string 可以从 const char * 隐式构造: • string s = “hello”; • string 具有 + 、 += 、 == 等直观的运算符重载: • string(“hello”) + string(“world”) == string(“helloworld”) • string 符合 vector 的接口,例如 begin/end/size/resize…… • 因此 string 类从 C 字符串构造时,可以额外指定一个长度: • string(“hello”, 3) 会得到 “ hel” • ↑ len 为 3 , ptr 指向 ’ h’ ,只保留前三个字符 • string(“hello”, 12) 会得到 “ hello\0[ 数据删除 ]” • ↑ len 为 12 , ptr 指向 ’ h’ ,超出了 6 个字符,内存读越界(出错) • string(“hello\0world0 码力 | 162 页 | 40.20 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 14 C++ 标准库系列课 - 你所不知道的 set 容器的排序: string 会按“字典序”来排 • set 会从小到大排序,对 int 来 说就是数值的大小比较。那么对 字符串类型 string 要怎么排序 呢? • 其实 string 类定义了运算符重 载 < ,他会按字典序比较两个 字符串。所谓字典序就是优先比 较两者第一个字符(按 ASCII 码比较),如果相等则继续比较 下一个,不相等则直接以这个比 较的结果返回。如果比到末尾都 相等且字符串长度一样,则视为 型,例如 int 或 string 等。 • 第二个 CompT 定义了你想 要的比较函子, set 内部会 调用这个函数来决定怎么排 序。 • 如果 CompT 不指定,默认 会直接用运算符 < 来比较。 • 这里我们定义个 MyComp 作为比较函子,和默认的一 样用 < 来比较,所以没有变 化。 set 的排序:自定义排序函数 • 恶搞一下,这里我们把比较 函子 MyComp set 的排序:自定义排序函数 • 首先搞懂 set 内部是怎么确定 两个元素 a 和 b 相等的: • !(a < b) && !(b < a) • 也就是说他 set 内部没有用到 == 运算符,而是调用了两次 比较函子来判断的。逻辑是: • 若 a 不小于 b 且 b 不小于 a ,则视为 a 等于 b ,所以 这就是为什么 set 只需要一个 比较函子,不需要相等函子的 原因。0 码力 | 83 页 | 10.23 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 12 从计算机组成原理看 C 语言指针https://www.bilibili.com/video/BV1fa411r7zp 课程 PPT 和代码: https://github.com/parallel101/course 请问下面这三段代码有什么错误? • float x = -3.14; • printf(“%f\n”, abs(x)); • char str[10]; • scanf(“%10s”, str); • int 指针,顾名思义,就是“指向”一个内存中的变量。 • 语法规定:任何类型 T 所对应的指针类型是 T* 。 • 可以通过 & 运算符获取一个变量的指针(地址) 。 • 可以通过 * 运算符访问指针指向的变量(左值) 。 • 因此指针指向了变量,通过指针的 * 运算符写入 的值,会造成原变量也改变,这正是指针的用法。 float 类型对应的指针类型: float* • 任何类型都有相应的指针类型。 地址 字节 指针 p 的内容实际上就是一个整数 4 ,也就是变量 x 中第一个字节的门牌号。 因为 int 类型的四个字节都是紧挨着,所以只需要知道第一个字节的地址就行了。 这样等会通过 * 运算符访问的时候,就可以访问从门牌号 4 开始的一连串四个字节组成的 int 。 注意这里的指针 p 只有四字节,这是 32 位系统上的情况。 如果是 64 位系统,指针 p 将会是八字节的。 指针的本质是内存地址0 码力 | 128 页 | 2.95 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 06 TBB 开启的并行编程之旅7.被忽视的访存优化:内存带宽与 cpu 缓存机制 8.GPU 专题: wrap 调度,共享内存, barrier 9.并行算法实战: reduce , scan ,矩阵乘法等 10.存储大规模三维数据的关键:稀疏数据结构 11.物理仿真实战:邻居搜索表实现 pbf 流体求解 12.C++ 在 ZENO 中的工程实践:从 primitive 说起 13.结业典礼:总结所学知识与优秀作业点评 封装好了: parallel_for 面向初学者: parallel_for 基于迭代器区间: parallel_for_each 二维区间上的 for 循环: blocked_range2d 三维区间上的 for 循环: blocked_range3d 所有区间类型 第 2 章:缩并与扫描 缩并( reduce ) 1 个线程,依次处理 8 个元素的缩并,花了 7 秒 用电量: 1*7=7 结论:串行扫描的时间复杂度为 O(n) ,工作复杂度为 O(n) 。 并行扫描 第一步、 4 个线程,每人处理 2 个元素的缩并,花了 1 秒 第二步、 1 个线程,独自处理 3 个元素的缩并,花了 3 秒 第三步、 3 个线程,每人处理 2 个元素的缩并,花了 1 秒 用电量: 4*1+1*3+3*1=10 度电 总用时: 1+3+1=5 秒 结论:并行扫描的时间复杂度为 O(n/c+c) ,工作复杂度为0 码力 | 116 页 | 15.85 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 08 CUDA 开启的 GPU 编程,前面加上 __global__ 修 饰符,即可让他在 GPU 上执行。 • 不过调用 kernel 时,不能直接 kernel() ,而 是要用 kernel<<<1, 1>>>() 这样的三重尖括 号语法。为什么?这里面的两个 1 有什么用 ?稍后会说明。 • 运行以后,就会在 GPU 上执行 printf 了。 • 这里的 kernel 函数在 GPU 上执行,称为核 函数,用 定义在 GPU 上的设备函数 • __global__ 用于定义核函数,他在 GPU 上执行,从 CPU 端通过三重尖括号语法调 用,可以有参数,不可以有返回值。 • 而 __device__ 则用于定义设备函数,他在 GPU 上执行,但是从 GPU 上调用的,而 且不需要三重尖括号,和普通函数用起来一 样,可以有参数,有返回值。 • 即: host 可以调用 global ; global 尚人文情怀的超绝境界”吸引投资人嘛。 第 1 章:线程与板块 三重尖括号里的数字代表什么意思? • 刚刚说了 CUDA 的核函数调用时需要用 kernel<<<1, 1>>>() 这种奇怪的语法,这里面 的数字代表什么意思呢? • 不妨把 <<<1, 1>>> 改成 <<<1, 3>>> 试试 看。你会看到 Hello, world! 打印了三遍! • 原来,三重尖括号里的第二个参数决定着启动 kernel0 码力 | 142 页 | 13.52 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 05 C++11 开始的多线程编程7.被忽视的访存优化:内存带宽与 cpu 缓存机制 8.GPU 专题: wrap 调度,共享内存, barrier 9.并行算法实战: reduce , scan ,矩阵乘法等 10.存储大规模三维数据的关键:稀疏数据结构 11.物理仿真实战:邻居搜索表实现 pbf 流体求解 12.C++ 在 ZENO 中的工程实践:从 primitive 说起 13.结业典礼:总结所学知识与优秀作业点评 日到当前时经过的秒数 • sleep(3); // 让程序休眠 3 秒 • long t1 = t0 + 3; // 当前时间的三秒后 • usleep(3000000); // 让程序休眠 3000000 微秒,也就是 3 秒 • C 语言原始的 API ,没有类型区分,导致很容易弄错单位,混淆时间点和时间段。 chrono::steady_clock::time_point 等 • 时间段类型: chrono::milliseconds , chrono::seconds , chrono::minutes 等 • 方便的运算符重载:时间点 + 时间段 = 时间点,时间点 - 时间点 = 时间段 • auto t0 = chrono::steady_clock::now();0 码力 | 79 页 | 14.11 MB | 1 年前3
C++高性能并行编程与优化 - 课件 - 03 现代 C++ 进阶:模板元编程7.被忽视的访存优化:内存带宽与 cpu 缓存机制 8.GPU 专题: wrap 调度,共享内存, barrier 9.并行算法实战: reduce , scan ,矩阵乘法等 10.存储大规模三维数据的关键:稀疏数据结构 11.物理仿真实战:邻居搜索表实现 pbf 流体求解 12.C++ 在 ZENO 中的工程实践:从 primitive 说起 13.结业典礼:总结所学知识与优秀作业点评 multiply(int) 作为虚函数。然后定义: Numeric *twice(Numeric *t) { return t->multiply(2); } 且不说这样的性能问题,你忍得住寂寞去重复定义好 几个,然后每个运算符都要声明一个纯虚函数吗? 而且, Float 的乘法应该是 multiply(float) ,你也去 定义好几个重载吗?定义为 multiply(Numeric *) 的话 依然会违背你们的开 - 闭原则:比如 调用了这个函数,才会被编译,才会报错! • 用一个假模板实现延迟编译的技术,可以加快编译的速度,用于代理模式等。 模板函数:一个例子 • 比如,要打印任意一个 vector : 模板函数:配合运算符重载 • 实现用 std::cout << a 打印任意 vector : 模板函数:大家学废了吗! 1. 类型作为参数: template2. 整数值作为参数: template 0 码力 | 82 页 | 12.15 MB | 1 年前3
共 37 条
- 1
- 2
- 3
- 4













