矢量化的堡垒限制

时间:2017-05-20 14:44:25

标签: fortran vectorization

我编写了一个函数,它返回一个矢量A,它等于稀疏矩阵Sparse与另一个矢量F的乘积。矩阵的非零值在Sparse(nnz),rowind(nnz)和colind(nnz)中)每个包含Sparse的每个特定值的行和列。通过kx下面的两行来对(现在注释的)内部循环进行矢量化是相对简单的....我无法看到如何对外循环进行矢量化,因为pos对于不同的kx具有不同的大小。

问题是:外环(do kx = 1,nxy)是否可以被矢量化,如果是,如何?

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %

Vladimir F正确地推测我来自Python / Octave世界。随着我解决的PDE越来越大,我已经移动(返回)到fortran以从我的硬件中获得更多性能。从半小时前开始,矢量化意味着摆脱do循环,这是fortran看起来非常擅长的东西:更换内部循环所需的时间节省" (做ky = 1,尺寸(pos)..)由上面的两行是令人惊讶的。当调用-fopt-info并查看循环修改时,我会查看gfortran(真正的gcc?)给出的信息。我将立即阅读有关SIMD和数组表示法的内容。如果有关于这个主题的好消息来源,请告诉我。

在回答Holz时,存在无数种方法来存储稀疏矩阵,通常导致操作符的等级降低1:我编写的示例包括在某些字段中的每个位置处评估的强制和解决方案向量,以及因此具有等级1.然后关联的运算符(S,如在A = S.F中)是二维BUT稀疏的。它以这样的方式存储,即只保留非零值。如果S中存在nnz非零值,则Sp(稀疏等效于S)为Sp(1:nnz)。如果pos表示某个数字Sp(pos)的序列中的位置,那么原始矩阵S中的列和行位置由colind(pos)和rowind(pos)给出。

在这样的背景下,我可能会将问题扩大到:完成乘法可以做的最好(按执行时间衡量)是什么?

pure function SparseMul(Sparse,F) result(A)
  implicit none
  integer (kind=4),allocatable :: pos(:)
  integer (kind=4) :: kx,ky   ! gp counters
  real (kind=8),intent(in) :: Sparse(:),F(:)
  real (kind=8),allocatable :: A(:)
  allocate(A(nxy))
  do kx=1,nxy                 !for each row
    pos=pack([(ky,ky=1,nnz)],rowind==kx)
    A(kx)=sum(Sparse(pos)*F(colind(pos)))
!!$       A(kx)=0
!!$       do ky=1,size(pos)
!!$          A(kx)=A(kx)+Sparse(pos(ky))*F(colind(pos(ky)))
!!$       end do
  end do
end function SparseMul

2 个答案:

答案 0 :(得分:1)

我假设问题"和#34;,即:

  • 我们不想更改矩阵存储格式
  • 我们不想使用外部库来执行任务

否则,我认为使用外部库应该 是解决问题的最佳方法,例如 https://software.intel.com/en-us/node/520797

要预测最好的"并不容易。 Fortran写的方式 乘法。这取决于几个因素(编译器,架构, 矩阵大小,...)。我认为最好的策略是提出建议 一些(合理的)尝试并在实际配置中测试它们。

如果我正确理解矩阵存储格式,我会提供我的尝试 - 包括问题中报告的那些 下面:

  1. 使用pack

    保存非零位置
    do kx=1,nxy
       pos=pack([(ky,ky=1,nnz)],rowind==kx)
       A(kx)=0
       do ky=1,size(pos)
          A(kx)=A(kx)+Sparse(pos(ky))*F(colind(pos(ky)))
       end do
    end do
    
  2. 作为前一个但使用Fortran数组语法

    do kx=1,nxy
       pos=pack([(ky,ky=1,nnz)],rowind==kx)
       A(kx)=sum(Sparse(pos)*F(colind(pos)))
    end do
    
  3. 使用条件来确定要使用的组件

    do kx=1,nxy
       A(kx)=0
       do ky=1,nnz
          if(rowind(ky)==kx) A(kx)=A(kx)+Sparse(ky)*F(colind(ky))
       end do
    end do
    
  4. 作为前一个但是互换循环

    A(:)=0
    do ky=1,nnz
       do kx=1,nxy
         if(rowind(ky)==kx) A(kx)=A(kx)+Sparse(ky)*F(colind(ky))
       end do
    end do
    
  5. 使用带有掩码参数

    的内在sum
    do kx=1,nxy    
       A(kx)=sum(Sparse*F(colind), mask=(rowind==kx))
    enddo
    
  6. 作为前一个,但使用隐含的do-loop

    A =[(sum(Sparse*F(colind), mask=(rowind==kx)), kx=1,nxy)]
    
  7. 这些是使用1000x1000矩阵的结果 33%的非零值。该机器是Intel Xeon 我的测试是使用英特尔v17和GNU 6.1编译器执行的 不使用优化,高优化但是 没有矢量化和高度优化。

              V1   V2   V3   V4   V5   V6
    -O0
    ifort    4.28 4.26 0.97 0.91 1.33 2.70
    gfortran 2.10 2.10 1.10 1.05 0.30 0.61
    
    -O3 -no-vec
    ifort    0.94 0.91 0.23 0.22 0.23 0.52
    gfortran 1.73 1.80 0.16 0.15 0.16 0.32
    
    -O3
    ifort    0.59 0.56 0.23 0.23 0.30 0.60
    gfortran 1.52 1.50 0.16 0.15 0.16 0.32
    

    对结果的一些简短评论:

    • 版本3-4-5通常是最快的
    • 编译器优化的作用对任何版本都至关重要
    • 矢量化似乎只对其起重要作用 非最佳版本
    • 版本4对两个编译器都是最好的
    • gfortran V4是"最佳" 版本
    • 优雅并不总是意味着良好的表现(V6不是很好)
    • 可以分析报告的其他评论 编译器优化。

    如果我们有多核机器,我们可以尝试使用 所有的核心。这意味着要处理代码并行化 是一个广泛的问题,但只是给一些提示让我们测试 两种可能的OpenMP并行化。我们致力于 串行最快的版本(尽管不能保证它 也是最好的并行版本。)

    OpenMP 1.

    !$omp parallel
    !$omp workshare
        A(:)=0
    !$omp end workshare
    !$omp do 
        do ky=1,nnz
          do kx=1,nxy                 !for each row
            if(rowind(ky)==kx) A(kx)=A(kx)+Sparse(ky)*F(colind(ky))
          end do
        end do
    !$omp end do 
    !$omp end parallel
    </pre>
    

    OpenMP 2.将firstprivate添加到只读向量以改善内存访问

    !$omp parallel firstprivate(Sparse, colind, rowind)
        ...
    !$omp end parallel
    

    这些是16个核心上最多16个线程的结果:

    #threads   1     2     4     8    16
    OpenMP v1
    ifort    0.22  0.14 0.088 0.050 0.027
    gfortran 0.155 0.11 0.064 0.035 0.020
    OpenMP v2
    ifort    0.24  0.12 0.065 0.042 0.029
    gfortran 0.157 0.11 0.052 0.036 0.029
    

    考虑到可扩展性(16个线程中大约8个)是合理的 它是一个内存限制计算。第一次私人优化 仅对少量线程有优势。 gfortran使用 16个线程是&#34;最佳&#34; OpenMP解决方案。

答案 1 :(得分:0)

我很难看到COLIND在哪里以及它在做什么......还有KX和KY。对于你需要矢量化的内部循环,这对我来说使用OpenMP SIMD REDUCTION来说似乎最简单。我具体看这里:

!!$       A(kx)=0
!!$       do ky=1,size(pos)
!!$          A(kx)=A(kx)+Sparse(pos(ky))*F(colind(pos(ky)))
!!$       end do

如果你必须聚集(PACK)那么它可能没什么用。如果F中有超过7/8的零,那么F可能更好于PACK。否则,向量乘以所有内容(包括零和)可能会更好。

主要规则是数据需要是连续的,因此你不能在第二维上进行矢量化......如果感觉稀疏和F是rank = 2,但它们显示为RANK = 1。即使它们实际上是rank = 2数组,这对于作为向量进行操作也很好。 UNION / MAP也可用于实现2D数组,同时也是一维向量。

Sparse和F真的排名= 1吗?什么是nax,nay,nxy和colind用于?其中许多都没有定义(例如nay,nnz和colind)