并行算法优化(1)

小结

通常支持超线程的多核处理器能够使用的线程数最多是物理核心数的2倍

X86 流加载/流存储:

__mm_stream_load
__mm256_stream_load
__mm_stream_store
__mm256_stream_store

SSE中的 prefetch指令 可以实现软件预取技术

NUMA技术:

提高多路系统中多核处理器之间通信的带宽。
原理:访问存储器的速度与距离处理器的距离有关,为了满足分配的从“近端”起。

  1. 线程内使用 malloc 分配
#pragma omp parallel
{
	int myid = omp_get_thread_num();
	data[i] = (float*) malloc(size);
	init(data[i]);
}
  1. 线程亲和性
    gcc的环境变量:GOMP_CPU_AFFINITY
    Linux系统:numactl工具
    OpenMP、pthread

性能分析工具:

gprof :通过在编译时插入代码来分析程序
nvprof :NVIDIA开发,用于运行在GPU上CUDA程序性能的工具
vampire Trace :基于命令行的并行程序剖分工具(vampire 图形化显示)
Intel VTune
perf: 跟踪内核调用,支持功耗剖分(软/硬件计数器)
(perf、gprof、valgrind对于串行程序剖分相当有用,但是并不适用于并行)
(vampire 剖分并行程序)

串行代码性能优化:

  1. 处理器利用率
    top 查看计算机各个核心的负载
  2. 存储器利用率
    测试存储器系统的可用带宽(stream工具)、程序实际带宽(硬件计数器)
    采用访存优化
  3. IO优化
    测试IO操作占用时间
    使用非阻塞函数的调用

编译选项(gcc):

-O3 -ffast-math -funroll-all-loops -mavx -mtune=native

fast-math: 对超越函数使用更快但精度低的版本
unroll-all-loops:使用循环展开
avx: AVX指令集向量化
tune=native 为当前编译的处理器做优化

高性能库:

BLAS 基本线性资程序
FFTW 快速傅里叶变换自由软件库

去调全局变量

受限指针:利用restrict(G++:restrict)限制符

条件编译:

条件编译生成的代码更短,因此效率更高
C:

#if#else#elif#endif#ifdef

生成的指令更短,对指令缓存的利用更好

算法级别优化:

缓存优化:

  1. 索引顺序
  2. 缓存分块
    通常以核心间不共享的最低层缓存分块
  3. 软件预取
  4. 查表法
    查表法和线性插值结合使用来减少计算精度的降低

函数级别的优化:

  1. 函数调用参数
    传指针或引用
  2. 内联函数(inline)
    通常对少于10行且其中代码没有分支的函数内联

循环级别的优化

  1. 循环展开
    通常展开小循环且内部没有判断的会收益 (展开大循环可能会造成寄存器溢出)
    对于二重循环,建议优先展开外层循环
  2. 循环累积
  3. 循环合并
  4. 循环拆分

语句级别的优化

  1. 减少内存读写
  2. 选用尽量小的数据类型 (short int)(特别运用在SSE/AVX指令)
  3. 结构体对齐
    声明结构体时,尽量大数据类型在前,小数据类型在后
    编码时尽量使用sizeof运算符
    占用总字节数为2的幂
    字节对齐的编译原语:
#GCC
__attribute__((aligned(x)))
__attribute__((packed()))
  1. 表达式移除(去重)
  2. 分支优化
    尽量避免将判断放入循环当中
    奇偶分支模型拆分循环,避免分支判断
for(int i=0;i<len;i++){
	if(i&1==0) do0();
	else do1();
}

// 1
for(int i=0;i<len-1;i+=2){
	do0();
	do1();
}
if(0 != len%2) do0();

// 2
for(int i=0;i<len;i+=2){
	do0();
}
for(int i=1;i<len;i+=2){
	do1();
}
if(0 != len%2) do0();

合并多个条件(要求在分支执行前计算出分支的结果)
使用条件状态生成掩码来移除条件分支
查表法移除分支
利用短路原来来确定分支顺序

  1. 优化交换性能
// swap
//优化前
unsigned char tmp = a[ji];
a[ji] = a[jj];
a[jj] = temp;

//优化后
unsigned char aji = a[ji];
unsigned char ajj = a[jj];
a[ji] = ajj;
a[jj] = aji;

//好处:消除了读写之间的相互依赖

指令级别的优化

  1. 减少数据依赖
//前缀和的优化
for(int i=1;i<len;i++){
	a[i]+=a[i-1]
}

//优化
float tmp=0.0f;
for(int i=0;i<len;i++){
	tmp+=a[i];
	a[i]=tmp;
}
  1. 注意处理器的多发射能力
  2. 优化乘除法和取模
    整数位移1个周期,乘法3个周期,除法十几个周期,模余运算几十个甚至上百个
    有关2的幂,转换成位运算
  3. 运用更具体的函数或算法
    求2的n次方应该采用pow2(n),而不是pow(2,n)

循环级依赖的优化

  1. 循环数据依赖优化
  2. 循环控制依赖优化

并行编程模型

  1. 指令级并行
//common use
sum =0;
for(int i=0;i<n;i+=4){
	sum += a[i];
	sum += a[i+1];
	sum += a[i+2];
	sum += a[i+3];
}

// 都依赖于sum的刷新(串行)
//优化
sum=sum1=sum2=sum3=0;
for(int i=0;i<n;i+=4){
	sum += a[i];
	sum1 += a[i+1];
	sum2 += a[i+2];
	sum3 += a[i+3];
}
sum += sum1;
sum2 += sum3;
sum += sum2;
  1. 向量化并行
  2. 易并行
  3. 任务并行
  4. 数据并行
  5. 循环并行化
  6. 区域分解并行
  7. 隐式和显式并行化
    隐式:OpenMP、OpenACC
    显式:pthreads
  8. SPMD
  9. 共享存储器并行

并行编程环境

支持隐式共享内存器并行的环境:OpenMP、OpenACC、SSE/AVX、NEON内置函数
支持显式共享内存器并行的环境:pthread、cilk、CUDA、OpenCL
支持显式消息传递并行的环境:MPI

并行方法论

并行算法设计的基本步骤:划分、通信、结果归并、负载均衡
操作的原子性、结果的可能性、函数的可重入性、顺序一致性
常见的并行程序通信方式:锁、临界区、原子操作、barrier、volatile关键字
静态负载均衡和动态负载均衡

参考资料

来源:并行算法设计与性能优化(刘文志)

你可能感兴趣的:(高性能计算,高性能计算,并行编程,性能优化)