鲲鹏社区首页
中文
注册
OpenMP优化调研系列文章(5)

OpenMP优化调研系列文章(5)

DevKit

发表于 2023/01/05

0

部分代码SIMD/并行化

该优化的灵感来源于ICC编译器的自动向量化功能[1],ICC编译器在做自动向量化的工作的时候,将一些不可SIMD的工作拆分成可以SIMD和不可SIMD两部分,使用临时数组存储可SIMD的计算结果,最后再将两部分结果结合在一起。我们也可将一段本不可并行运行的工作拆成可以并行化和不可并行化两部分进行优化,并且OpenMP如今也支持SIMD功能,同样也可以做部分SIMD。我们调研了使用手写代码完成上述两种优化的测试,发现在计算量足够大,或者是可以多次重复利用临时数组的时候,可以得到性能的优化。

优化前:

subroutine f(a, b, c, d)
  integer :: a(10000000), b(10000000), c(10000000), d(10000000)
  do i = 2, 10000000
    a(i) = a(i - 1) + b(i) * c(i) / d(i) * b(i)
  end do
end

program main
  integer :: a(10000000), b(10000000), c(10000000) 
  do i = 1, 100
    call f(a, b, c, d)
  end do
end program main

优化后:

subroutine f(a, b, c, d, e)
  integer :: a(10000000), b(10000000), c(10000000), d(10000000), e(10000000)
  !$omp parallel do            //or use omp simd
  do i = 2, 10000000
    d(i) = b(i) * c(i) / e(i) * b(i)
  end do
  !$omp end parallel do
  do i = 2, 10000000
    a(i) = a(i - 1) + d(i)
  end do
end

program main
  integer :: a(10000000), b(10000000), c(10000000), d(10000000) 
  do i = 1, 100
    call f(a, b, c, d, e)
  end do
end program main

Optimization training at CINES[2]

1. 针对搜索算法的优化

论文作者针对搜索算法优化了OpenMP,其调研的OpenMP版本较老,最新的版本OpenMP提供了新功能完善了论文作者描述的代码,首先看论文作者的优化方法:

优化前:

bool searchValV1(const float *A, const int n, float val)
{
  bool found = false;
  #pragma omp parallel shared(A, found) firstprivate(val)
  {
    #pragma omp for schedule(static,16)
    {
      for(int i = 0; i < n; i++) {
        if(A[i] == val)
          found = true;
      }
    }
  }
  return found;
}

由于OpenMP的标准规定不允许使用break这样的中断语句结束循环,导致原代码做了很多重复无用的工作,因此论文作者使用了以下代码,这样线程在已经查到之后将不再对A数组进行访问,提高了性能。

优化后:

bool searchValV1(const float *A, const int n, float val) 
{
  bool found = false;
  #pragma omp parallel shared(A, found) firstprivate(val)
  {
    #pragma omp for schedule(static,16)
    {
      for(int i = 0; i < n; i++) {
        if(!found)
          if(A[i] == val)
            found = true;
      }
    }
  }
  return found;
}

而如今的OpenMP提供了cancel指令,可以指定并行域的取消,现在使用该指令完成如上代码的功能:

bool searchValV1(const float *A, const int n, float val) 
{
  bool found = false;
  #pragma omp parallel shared(A, found) firstprivate(val)
  {
    #pragma omp for schedule(static,16)
    {
      for(int i = 0; i < n; i++) {
        if(A[i] == val)
          {
            found = true;
            #pragma omp cancel for
          }
      }
    }
  }
  return found;
}

2. 针对临界区的减少同步次数

当多个线程都可能对一个共享变量进行修改的时候,在对其进行修改的时候,要保证在临界区域对其进行修改,防止出现错误。

float kernelV2(const float *A, float *B, const int n)
{
  float minVal = INF;
  #pragma omp parallel shared(A, B, minVal) firstprivate(n)
  {
    #pragma omp for schedule(static,16)
    {
      for(int i = 0; i < n; i++) {
        B[i] = 0.5f * A[i];
        #pragma omp critical
        {
          if(B[i] < minVal)
            minVal = B[i];
        }
      }
    }
  }
  return minVal;
}

在这种带有判断条件的临界区时,论文作者提出可以将其判断语句外延至临界区外以减少进入临界区的次数,但是又不可删除临界区中的判断条件,防止出现错误。

float kernelV2(const float *A, float *B, const int n)
{
  float minVal = INF;
  #pragma omp parallel shared(A, B, minVal) firstprivate(n)
  {
    #pragma omp for schedule(static,16)
    {
      for(int i = 0; i < n; i++) {
        B[i] = 0.5f * A[i];
        // when B[i] >= minVal, there is no
        // threads synchronisation
        if(B[i] < minVal)
          #pragma omp critical
          {
          // this is very important to re-do the
          // test because another thread may
          // modify the minVal value
            if(B[i] < minVal)
              minVal = B[i];
          }
      }
    }
  }
  return minVal;
}

基于键涨落模型数值模拟的并行优化[3]

通过改变变量传递方式提高Cache命中率

先看以下代码:

#pragma omp parallel default(shared) private(tid) shared(a,b)
{
  tid = omp_get_thread_num();
  work(a[tid], b[tid]);
}

a、b数组为一维数组,元素个数为最大线程数,每个线程根据tid为数组索引访问a、b中不同元素,work函数参数传递方式为传递引用,函数体内存在大量对数组的写回操作,在这种情况下当线程数规模拓展到一定程度,数据一致性导致的Cache冲刷大大降低了Cache的命中率,一个有效的手段就是尽可能使用线程私有变量的读写代替对数组元素的读写。以下给出优化代码:

#pragma omp parallel default(shared) private(ia, ib, tid) shared(a, b)
  {
    tid = omp_get_thread_num;
    ia = a[tid];
    ib = b[tid];
    work(ia, ib);
    a[tid] = ia;
    a[tid] = ib;
  }

利用OpenMP线程绑定技术提升多核平台应用性能[4]

线程绑定

在多核平台中,线程在核中的动态迁移在一定程度上会导致应用性能下降。如果将线程绑定在固定的核上进行,使其不再迁移,这种方法可以提升程序性能。论文作者通过对STREAM这一benchmark进行测试并对结果进行分析得到结论:

如果OpenMP程序需要大量的访存操作,使用到了大量的Cache,将线程均匀地绑定到各个CPU内部,可以显著提升程序性能;如果OpenMP程序在每个线程需要使用到大量的共享内存,这些线程在进行绑定的时候需要尽量聚集在一个CPU的内部。当然,线程绑定实际上禁用了操作系统的负载平衡功能,可能会出现某些CPU繁忙而某些CPU核空闲的情况。

Reference

1. https://www.intel.com/content/www/us/en/developer/articles/tool/oneapi-standalone-components.html#dpcpp-cpp

2. https://www.cines.fr/wp-content/uploads/2014/10/lesson3_slides.pdf

3. 孙志刚. 基于键涨落模型数值模拟的并行优化[D].山东大学,2013.

4. https://www.docin.com/p-51138679.html

作者介绍


谢依晖

湖南大学硕士研究生在读,本科毕业于湖南大学计算机科学与技术专业

本页内容