 C++高性能并行编程与优化 -  课件 - 12 从计算机组成原理看 C 语言指针64 位的,一些很老的网吧和学校的机房里偶尔能看见古董级的 32 位计算机, 16 位计算机则是几乎只能在博物馆里看到了。 • 字的长度决定了计算机中寄存器的大小,从而决定计算机一次能处理多大的整数。 • 例如 32 位计算机的寄存器都是 32 位,因此只能做 32 位整数的加减乘除,超过 32 位 整数的加减乘除就要用特殊的指令来模拟了。 整数的表示范围受位数限制 • 8 位长的整数能表示的范围是 范围的整数,也就是两个字节组成的字。 • 处理器去读写内存的时候靠的是寄存器提供的地址,因此寄存器的大小(也就是字的大 小)决定了他能读写的内存大小,例如: • 由于 16 位计算机的寄存器只能存储 16 位,他只能访问 65536 字节( 64 KB )的内存 。 • 由于 32 位计算机的寄存器只能存储 32 位,他只能访问 4 GB 的内存。 • 由于 64 位计算机的寄存器能存储 64 位,他理论上能访问 16777216 • 因此,如果你的电脑内存超过了 4 GB ,那肯定是 32 位电脑不用说了。 • 而 64 位计算机理论上能访问如此大量的内存,虽然目前看来是用不到。 知识拓展 • 虽然 64 位计算机的寄存器能处理 64 位的整数,实际上的内存地址并没有 64 位。 • 实际上地址的高 16 位始终和第 48 位一致(符号扩展),也就是虚拟地址空间只有 48 位。 • 而经过 MMU 映射后实际给内存的地址只有0 码力 | 128 页 | 2.95 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 12 从计算机组成原理看 C 语言指针64 位的,一些很老的网吧和学校的机房里偶尔能看见古董级的 32 位计算机, 16 位计算机则是几乎只能在博物馆里看到了。 • 字的长度决定了计算机中寄存器的大小,从而决定计算机一次能处理多大的整数。 • 例如 32 位计算机的寄存器都是 32 位,因此只能做 32 位整数的加减乘除,超过 32 位 整数的加减乘除就要用特殊的指令来模拟了。 整数的表示范围受位数限制 • 8 位长的整数能表示的范围是 范围的整数,也就是两个字节组成的字。 • 处理器去读写内存的时候靠的是寄存器提供的地址,因此寄存器的大小(也就是字的大 小)决定了他能读写的内存大小,例如: • 由于 16 位计算机的寄存器只能存储 16 位,他只能访问 65536 字节( 64 KB )的内存 。 • 由于 32 位计算机的寄存器只能存储 32 位,他只能访问 4 GB 的内存。 • 由于 64 位计算机的寄存器能存储 64 位,他理论上能访问 16777216 • 因此,如果你的电脑内存超过了 4 GB ,那肯定是 32 位电脑不用说了。 • 而 64 位计算机理论上能访问如此大量的内存,虽然目前看来是用不到。 知识拓展 • 虽然 64 位计算机的寄存器能处理 64 位的整数,实际上的内存地址并没有 64 位。 • 实际上地址的高 16 位始终和第 48 位一致(符号扩展),也就是虚拟地址空间只有 48 位。 • 而经过 MMU 映射后实际给内存的地址只有0 码力 | 128 页 | 2.95 MB | 1 年前3
 C++高性能并行编程与优化 -  课件 - 08 CUDA 开启的 GPU 编程读取 sum[0] 到寄存器 A • 读取 arr[i] 到寄存器 B • 让寄存器 A 的值加上寄存器 B 的值 • 写回寄存器 A 到 sum[0] • 这样有什么问题呢? 经典案例:数组求和 • 假如有两个线程分别在 i=0 和 i=1 ,同时执行: • 线程 0 :读取 sum[0] 到寄存器 A ( A=0 ) • 线程 1 :读取 sum[0] 到寄存器 A ( A=0 ) arr[0] 到寄存器 B ( B=arr[0] ) • 线程 1 :读取 arr[1] 到寄存器 B ( B=arr[1] ) • 线程 0 :让寄存器 A 加上寄存器 B ( A=arr[0] ) • 线程 1 :让寄存器 A 加上寄存器 B ( A=arr[1] ) • 线程 0 :写回寄存器 A 到 sum[0] ( sum[0]=arr[0] ) • 线程 1 :写回寄存器 A 到 sum[0] 中有的为真有的为假,则会导致两个分 支都被执行!不过在 cond 为假的那几个线程 在真分支会避免修改寄存器和访存,产生副作 用。而为了避免会产生额外的开销。因此建议 GPU 上的 if 尽可能 32 个线程都处于同一个 分支,要么全部真要么全部假,否则实际消耗 了两倍时间! 避免修改寄存器和访存相当于 CPU 的 SIMD 指令 _mm_blendv_ps 和 _mm_store_mask_ps0 码力 | 142 页 | 13.52 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 08 CUDA 开启的 GPU 编程读取 sum[0] 到寄存器 A • 读取 arr[i] 到寄存器 B • 让寄存器 A 的值加上寄存器 B 的值 • 写回寄存器 A 到 sum[0] • 这样有什么问题呢? 经典案例:数组求和 • 假如有两个线程分别在 i=0 和 i=1 ,同时执行: • 线程 0 :读取 sum[0] 到寄存器 A ( A=0 ) • 线程 1 :读取 sum[0] 到寄存器 A ( A=0 ) arr[0] 到寄存器 B ( B=arr[0] ) • 线程 1 :读取 arr[1] 到寄存器 B ( B=arr[1] ) • 线程 0 :让寄存器 A 加上寄存器 B ( A=arr[0] ) • 线程 1 :让寄存器 A 加上寄存器 B ( A=arr[1] ) • 线程 0 :写回寄存器 A 到 sum[0] ( sum[0]=arr[0] ) • 线程 1 :写回寄存器 A 到 sum[0] 中有的为真有的为假,则会导致两个分 支都被执行!不过在 cond 为假的那几个线程 在真分支会避免修改寄存器和访存,产生副作 用。而为了避免会产生额外的开销。因此建议 GPU 上的 if 尽可能 32 个线程都处于同一个 分支,要么全部真要么全部假,否则实际消耗 了两倍时间! 避免修改寄存器和访存相当于 CPU 的 SIMD 指令 _mm_blendv_ps 和 _mm_store_mask_ps0 码力 | 142 页 | 13.52 MB | 1 年前3
 C++高性能并行编程与优化 -  课件 - 04 从汇编角度看编译器优化章:汇编语言 x64 架构下的寄存器模型 通用寄存器: 32 位时代 • 32 位 x86 架构中的通用寄存器有: • eax, ecx, edx, ebx, esi, edi, esp, ebp • 其中 esp 是堆栈指针寄存器,和函数的调用与返回相关。 • 其中 eax 是用于保存返回值的寄存器。 通用寄存器: 64 位时代 • 64 位 x86 架构中的通用寄存器有: • rax, rcx rdx, rbx, rsi, rdi, rsp, rbp, r8, r9, r10, r11, ..., r15 • 其中 r8 到 r15 是 64 位 x86 新增的寄存器,给了汇编程序员更大的空间,降低了编译 器处理寄存器翻车( register spill )的压力。 • 因此 64 位比 32 位机器相比,除了内存突破 4GB 限制外,也有一定性能优势。 8 位, 16 位, 32 INT_MAX 的情况,推荐始终用 size_t 表示数组大小和索引。 浮点作为参数和返回: xmm 系列寄存器 xmm0 = xmm0 + xmm1 参数分别通过 xmm0 , xmm1 传入。 返回值通过 xmm0 传出。 什么是 xmm 系列寄存器? • xmm 寄存器有 128 位宽。 • 可以容纳 4 个 float ,或 2 个 double 。 • 刚才的案例中只用到了0 码力 | 108 页 | 9.47 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 04 从汇编角度看编译器优化章:汇编语言 x64 架构下的寄存器模型 通用寄存器: 32 位时代 • 32 位 x86 架构中的通用寄存器有: • eax, ecx, edx, ebx, esi, edi, esp, ebp • 其中 esp 是堆栈指针寄存器,和函数的调用与返回相关。 • 其中 eax 是用于保存返回值的寄存器。 通用寄存器: 64 位时代 • 64 位 x86 架构中的通用寄存器有: • rax, rcx rdx, rbx, rsi, rdi, rsp, rbp, r8, r9, r10, r11, ..., r15 • 其中 r8 到 r15 是 64 位 x86 新增的寄存器,给了汇编程序员更大的空间,降低了编译 器处理寄存器翻车( register spill )的压力。 • 因此 64 位比 32 位机器相比,除了内存突破 4GB 限制外,也有一定性能优势。 8 位, 16 位, 32 INT_MAX 的情况,推荐始终用 size_t 表示数组大小和索引。 浮点作为参数和返回: xmm 系列寄存器 xmm0 = xmm0 + xmm1 参数分别通过 xmm0 , xmm1 传入。 返回值通过 xmm0 传出。 什么是 xmm 系列寄存器? • xmm 寄存器有 128 位宽。 • 可以容纳 4 个 float ,或 2 个 double 。 • 刚才的案例中只用到了0 码力 | 108 页 | 9.47 MB | 1 年前3
 C++高性能并行编程与优化 -  课件 - 05 C++11 开始的多线程编程经典案例:多个线程修改同一个计数器 • 多个线程同时往一个 int 变量里累加,这样肯定会出错 ,因为 counter += i 在 CPU 看来会变成三个指令: 1. 读取 counter 变量到 rax 寄存器 2. rax 寄存器的值加上 1 3. 把 rax 写入到 counter 变量 • 即使编译器优化成 add [counter], 1 也没用,因为现代 CPU 为了高效,使用了大量奇技淫巧,比如他会把一 经典案例:多个线程修改同一个计数器(续) • 问题是,如果有多个线程同时运行,顺序是不确定的: 1. t1 :读取 counter 变量,到 rax 寄存器 2. t2 :读取 counter 变量,到 rax 寄存器 3. t1 : rax 寄存器的值加上 1 4. t2 : rax 寄存器的值加上 1 5. t1 :把 rax 写入到 counter 变量 6. t2 :把 rax 写入到 counter 变量0 码力 | 79 页 | 14.11 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 05 C++11 开始的多线程编程经典案例:多个线程修改同一个计数器 • 多个线程同时往一个 int 变量里累加,这样肯定会出错 ,因为 counter += i 在 CPU 看来会变成三个指令: 1. 读取 counter 变量到 rax 寄存器 2. rax 寄存器的值加上 1 3. 把 rax 写入到 counter 变量 • 即使编译器优化成 add [counter], 1 也没用,因为现代 CPU 为了高效,使用了大量奇技淫巧,比如他会把一 经典案例:多个线程修改同一个计数器(续) • 问题是,如果有多个线程同时运行,顺序是不确定的: 1. t1 :读取 counter 变量,到 rax 寄存器 2. t2 :读取 counter 变量,到 rax 寄存器 3. t1 : rax 寄存器的值加上 1 4. t2 : rax 寄存器的值加上 1 5. t1 :把 rax 写入到 counter 变量 6. t2 :把 rax 写入到 counter 变量0 码力 | 79 页 | 14.11 MB | 1 年前3
 C++高性能并行编程与优化 -  课件 - 07 深入浅出访存优化存器资源。 使用 _mm_stream_ps 和 SIMD 指令,加速计算和直写 • 为了充分填满寄存器,我们把 t 循环和 offset 循环交换一下( loop-interchange ) ,把 offset 换到内层循环去。这样至少能 让四个寄存器同时在进行加法运算( xmm 寄存器最多有几个来着?总之也不能太多, 不然被编译器 spill 到内存就不好了),从 而让 CPU 能够发现并启动指令级并行 AVX 指令:宽度为 8 的浮点矢量 • __m128 一次处理四个 float ,改成 __m256 一次处理八个 float ,应该会更快吧?的确变 快了。 • 不过因为 res 有四个寄存器, 4*4*8=128 字节 ,所以 _mm_prefetch 需要预取两个缓存行 才行。 • 顺便,这里 blockSize 和 32 似乎一样了, 所以 xBase 也可以直接去掉了。 • c(t, j) 连续的顺序访问(好)。 • 因为存在不连续的 b 和一直不动的 a , 导致矢量化失败,一次只能处理一个标量 , CPU 也无法启动指令级并行( ILP )。 解决:寄存器分块(类似于循环分块) • 分析访存规律: • a(i, j) 连续 32 次顺序访问(好)。 • b(i, t) 连续 32 次顺序访问(好)。 • c(t, j) 32 次在一个地址不动(一般)。0 码力 | 147 页 | 18.88 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 07 深入浅出访存优化存器资源。 使用 _mm_stream_ps 和 SIMD 指令,加速计算和直写 • 为了充分填满寄存器,我们把 t 循环和 offset 循环交换一下( loop-interchange ) ,把 offset 换到内层循环去。这样至少能 让四个寄存器同时在进行加法运算( xmm 寄存器最多有几个来着?总之也不能太多, 不然被编译器 spill 到内存就不好了),从 而让 CPU 能够发现并启动指令级并行 AVX 指令:宽度为 8 的浮点矢量 • __m128 一次处理四个 float ,改成 __m256 一次处理八个 float ,应该会更快吧?的确变 快了。 • 不过因为 res 有四个寄存器, 4*4*8=128 字节 ,所以 _mm_prefetch 需要预取两个缓存行 才行。 • 顺便,这里 blockSize 和 32 似乎一样了, 所以 xBase 也可以直接去掉了。 • c(t, j) 连续的顺序访问(好)。 • 因为存在不连续的 b 和一直不动的 a , 导致矢量化失败,一次只能处理一个标量 , CPU 也无法启动指令级并行( ILP )。 解决:寄存器分块(类似于循环分块) • 分析访存规律: • a(i, j) 连续 32 次顺序访问(好)。 • b(i, t) 连续 32 次顺序访问(好)。 • c(t, j) 32 次在一个地址不动(一般)。0 码力 | 147 页 | 18.88 MB | 1 年前3
 C++高性能并行编程与优化 -  课件 - 性能优化之无分支编程 Branchless Programmingbool 类型和 char 一样只占据 1 字节( al 寄存器就 1 字节) • 而 C 语言可以自动把 bool 转换成 int 类型( movzx 把 1 字节的 al 转换成 4 字节的 eax ,零扩展:高 3 字节 填充零) • 返回类型 int 占据 4 字节( eax 寄存器就是 4 字节的) • 返回值都放 eax 寄存器(刚刚算得的就在 eax ,直接返 回) 无分支优化:从语法角度分析0 码力 | 47 页 | 8.45 MB | 1 年前3 C++高性能并行编程与优化 -  课件 - 性能优化之无分支编程 Branchless Programmingbool 类型和 char 一样只占据 1 字节( al 寄存器就 1 字节) • 而 C 语言可以自动把 bool 转换成 int 类型( movzx 把 1 字节的 al 转换成 4 字节的 eax ,零扩展:高 3 字节 填充零) • 返回类型 int 占据 4 字节( eax 寄存器就是 4 字节的) • 返回值都放 eax 寄存器(刚刚算得的就在 eax ,直接返 回) 无分支优化:从语法角度分析0 码力 | 47 页 | 8.45 MB | 1 年前3
共 6 条
- 1













