多线程编程库
- pthread, POSIX标准规定了UNIX及Linux实现的多线程接口pthread。pthread在所有linux下可用,业界广泛使用。
- win32 thread,Windows系统内置的线程API
- OpenMP,一个开放的,基于编译制导语句的API,目前各大操作系统上均可用,由于与操作系统平台无关,提供了比较好的可移植性,因此应当优先使用。
- C/C++标准线程
- OpenCL和CUDA,它们是基于GPU(OpenCL正在用于FPGA上编程)的并行计算架构、语言和API,由于在某些问题上比CPU更快的解决问题,目前广泛使用,适用范围正在逐渐扩大。
- OpenACC,基于加速器的编译制导标准,通常用于GPU、FPGA等的并行编程
多核上多线程并行需要注意的问题
- 线程过多: 频繁上下文切换,减低性能,内存污染
- 数据竞争
- 死锁
- 饿死
- 伪共享
缓存一致性策略 MESI
- M (modified),更改,表示缓存中数据已更改,在未来的某个时刻将写入内存
- E (exclusive),排除,表示缓存的数据只被当前的核心所缓存
- S (shared), 共享,表示缓存的数据还被其他核心缓存
- I (invaid),无效,表示缓存中的数据已经失效,其他核心更改了数据
NUMA技术
Intel处理器,多路处理器之间通过QPI总线通信;AMD处理器通过HT总线通信
QPI和HT的带宽比内存带宽小,延迟大于访问内存延迟,因此这是NUMA存在的原因
…
算法性能
- 读写数据内存墙
- 不同算法不同的限制因素,有些算法的大部分性能限制在读取网络数据上,有些算法限制在TLB上
TLB Translation Lookaside Buffer 传输后备缓冲器,一个内存管理单元,改进虚拟内存到物理地址内存转换速度的缓存,如果没有TLB,每次取数据都需要访问两次内存,即查页表获得物理地址和取数据 - 不同处理器执行不同指令的速度差异,生产商将更多的晶体管用于更常见的命令
计时方法
C标准库的time、clock系列函数,CUDA事件,Linux下的各塔timeofday,Windows下的GetTickCountdeng
通常使用clock和getTimOfSecond函数已够
程序性能分析实用工具
串行程序的剖分:Linux下的工具 perf、gprof、vargrind,其他比较著名且简单的有VS2010自带的剖分器和Intel Vtune工具
并行程序的剖分:TAU和Vampire,Vampire不仅支持MPI还支持OpenMP和pthread
- gprof
GNU编译器工具包提供的性能分析工具,能够给出调用关系,调用次数,执行时间等信息,大多数linux发行版默认安装了gprof。gprof通常和gcc/g++配合使用,添加选项 -pg -g (pg表示产生的程序可以用gprof分析),gcc/g++会自动在程序中插入一些代码以保存函数执行时间、执行次数、调用关系等。
- 参考手册 http://sourceware.org/binutils/docs/gprof/
- 参考博客 https://blog.csdn.net/luchengtao11/article/details/74910585
- nvprof
NVIDIA 开发的、用于分析运行在其GPU上的CUDA程序性能的工具。目前只支持运行在NVIDIA GPU 上的内核的分析
串行代码的性能优化
- 系统级别,找出程序性能控制因素,以做针对性的优化
- 应用级别,代码编写前确定应用级别的配置,比较简单,实现后性能比较稳定
- 算法级别,数据组织方式,算法对性能影响作用非常大
- 函数级别,减少函数调用开销,或者减少函数调用带来的编译器性能优化阻碍
- 循环级别,发掘循环并行性,减少循环内冗余计算
- 语句级别,尽量使用语句条数少的语句
- 指令级别,优先使用吞吐量大延迟小的指令
系统级别
- 如果应用通过网络互连,交换数据或指令,那么网速,利用率,网络负载均衡
- 处理器利用率:top命令输入1后
- 第二行:Tasks表示系统目前总共有多少进程,多少进程正在执行,多少进程正在休眠,多少进行结束,多少僵尸进程。
- 中间格列:表示各个核心上的占用情况,第一列逻辑核心编号,第二列用户空间对核心的占用率,第三列系统空间对核心的占用率
- Mem行:表示系统内存信息
- 存储器带宽利用率:
- 提高存储器访问的局部性以增加缓存利用,二维数据C语言优先访问行,Fortran语言优先访问列
- 通识读写多个数据
- 减少读写依赖
- 将数据保存到临时变量以减少存储器的读写,临时变量通常占用空间小,硬件能够将它们缓存到一级缓存,或编译器能够将他们分配到存储器中,避免访问更慢的存储器
- 减少io操作
应用级别
- 编译器选项
如GCC有O0(编译器对程序不做优化),O1、O2、O3(为了性能做极致优化)优化选项,还有指定处理器架构,是否使用循环展开,是否使用SSE等优化选项 使用GCC时建议使用如下编译选项
-O3 -ffast-math -funroll-all-loops -mavx -mtune=native
# 其中,fast-math表示对超越函数使用更快但精度更低一些的版本;
# unroll-all-loops表示使用循环展开;
# avx表示使用avx指令集向量化;
# tune=native表示为当前编译的处理器做优化。
- 调用高性能库
- 去掉全局变量,应当是绝对禁止的,因为多个控制流需要协调对全局变量的更改,如何保证一致,是并行编程的难点,解决这个难点的最好策略是回避他
- 受限的指针
- 条件编译
算法级别
- 索引顺序
# 优化前的代码
for(int i=0; i<N; i++){
for(int j=0; j<M; j++){
r[j] += a[j][i];
}
}
# 优化后的代码
for(int i=0; i<N; i++)
{
float ret = 0.0f;
for(int j=0; j<M ;j++)
{
ret += a[i][j];
}
r[i] = ret;
}
优化前,内层循环上,相邻循环体内访问a的地址相隔N个单元,在N比较大的情况下,可能会存在满不命中和冲突不命中的情况,访问a的局部性很差。理想情况下,编译器自己能够做这种形式的代码转换,但是如果循环代码很复杂,再优秀的编译器可能也无能为力。
2. 缓存分块
# 缓存分块代码示例: 矩阵乘法
for(i=0;i<N;i+=NB){
for(j=0;j<M;j+=NB){
for(k=0;k<K;k+=NB){
for(i0=i;i0<i+NB;i0++){
for(j0=j;j0<j+NB;j0++){
for(k0=k;k0<K;k0++){
C[i0][j0]+=A[i0][k0]+B[i0][k0]
}
}
}
}
}
}
如果数据超过了缓存大小,容易出现不同层次缓存不命中的情况
3. 软件预取
4. 查表法
提前把数据组织成表格,预先计算