【问题标题】:Fortran multidimensional sub-array performanceFortran 多维子阵列性能
【发布时间】:2015-09-03 20:05:30
【问题描述】:

在 Fortran90 中操作和分配多维数组中的子数组时,我偶然发现了一个有趣的性能怪癖。

Fortran90 引入了操作数组子部分的能力,我看到一些地方建议使用这种“切片”方法而不是循环来执行数组操作。例如,如果我必须添加两个数组,ab,大小为 10,最好这样写:

c(1:10) = a(1:10) + b(1:10)

c = a + b

代替

do i = 1, 10
    c(i) = a(i) + b(i)
end do

我对简单的一维和二维数组尝试了这种方法,发现使用“切片”表示法更快。然而,当在多维数组中分配这样的结果时,事情开始变得有点有趣了。

首先,我必须为我相当粗暴的性能测量练习道歉。我什至不确定我采用的方法是否是时间和测试代码的正确方法,但我对测试的定性结果相当有信心。

program main
    implicit none

    integer, parameter :: mSize = 10000
    integer :: i, j
    integer :: pCnt, nCnt, cntRt, cntMx
    integer, dimension(mSize, mSize) :: a, b
    integer, dimension(mSize, mSize, 3) :: c

    pCnt = 0
    call SYSTEM_CLOCK(nCnt, cntRt, cntMx)
    print *, "First call: ", nCnt-pCnt
    pCnt = nCnt

    do j = 1, mSize
        do i = 1, mSize
            a(i, j) = i*j
            b(i, j) = i+j
        end do
    end do

    call SYSTEM_CLOCK(nCnt, cntRt, cntMx)
    print *, "Created Matrices: ", nCnt-pCnt
    pCnt = nCnt

    ! OPERATIONS BY SLICING NOTATION
    !c(1:mSize, 1:mSize, 1) = a + b
    !c(1:mSize, 1:mSize, 2) = a - b
    !c(1:mSize, 1:mSize, 3) = a * b

    ! OPERATIONS WITH LOOP
    do j = 1, mSize
        do i = 1, mSize
            c(i, j, 1) = a(i, j) + b(i, j)
            c(i, j, 2) = a(i, j) - b(i, j)
            c(i, j, 3) = a(i, j) * b(i, j)
        end do
    end do

    call SYSTEM_CLOCK(nCnt, cntRt, cntMx)
    print *, "Added Matrices: ", nCnt-pCnt
    pCnt = nCnt
end program main

可以看出,我有两种方法可以对两个大型 2D 数组进行操作并将其分配到一个 3D 数组中。我非常赞成使用切片符号,因为它可以帮助我编写更短、更优雅的代码。但是在观察到我的代码有多么缓慢时,我不得不重新检查切片符号相对于循环内计算的能力。

我使用适用于 Ubuntu 14.04 的 GNU Fortran 4.8.4 运行带有和不带有 -O3 标志的上述代码

  1. 没有 -O3 标志

    一个。切片符号

         5 Runs - 843, 842, 842, 841, 859
    
         Average - 845.4
    

    b.循环计算

         5 Runs - 1713, 1713, 1723, 1711, 1713
    
         Average - 1714.6
    
  2. 带有 -O3 标志

    一个。切片符号

         5 Runs - 545, 545, 544, 544, 548
    
         Average - 545.2
    

    b.循环计算

         5 Runs - 479, 477, 475, 472, 472
    
         Average - 475
    

我发现非常有趣的是,如果没有 -O3 标志,切片符号的性能仍然比循环好得多。但是,使用 -O3 标志会导致这种优势完全消失。相反,在这种情况下使用数组切片表示法是有害的。

事实上,对于我相当大的 3D 并行计算代码,这将成为一个重要的瓶颈。我强烈怀疑在将低维数组分配给高维数组期间形成临时数组是罪魁祸首。但是为什么优化标志在这种情况下没有优化赋值呢?

而且,我觉得责备-O3标志不是一件值得尊敬的事情。那么数组临时对象真的是罪魁祸首吗?还有什么我可能会丢失的吗?任何见解都将对加快我的代码速度非常有帮助。谢谢!

【问题讨论】:

  • 您能使用更新版本的 gfortran 吗?
  • 查看-O3 暗示的选项。如果您询问,编译器可以进行积极的循环展开,这可能就是您所看到的(例如,您的循环案例不再是循环)。
  • 在这种情况下,我认为编译器不需要制作临时数组。
  • 另外,您应该初始化变量并打印结果。否则,我的 gfortran 只报告第二部分需要 0 毫秒,因为它在时间检查之前对其进行了优化或移动。
  • @AlexanderVogt 我可以使用更新版本的 gfortran,但最后,我必须将代码提交给我的大学集群,而我的 fortran 版本实际上不在我的控制范围内。我首先在集群上观察到了这种速度差异。

标签: performance multidimensional-array fortran


【解决方案1】:

在进行任何性能比较时,您必须将苹果与苹果、橙与橙进行比较。我的意思是,您并没有真正比较同一件事。即使它们产生相同的结果,它们也是完全不同的。

这里发挥作用的是内存管理,想想运行期间的缓存故障。如果您按照 haraldkl 的建议将循环版本转换为 3 个不同的循环,您肯定会获得类似的性能。

发生的情况是,当您在同一个循环中组合 3 个赋值时,右侧有很多缓存重用,因为所有 3 个在右侧共享相同的变量。对于循环版本,ab 的每个元素仅被加载到缓存和寄存器中一次,而对于数组操作版本,ab 的每个元素被加载 3 次。这就是与众不同的地方。数组越大,差异越大,因为你会得到更多的缓存错误和更多的元素重新加载到寄存器中。

【讨论】:

  • @haraldkl,嗯,这就是我的解释,可能不完整。
  • @VladimirF,虽然我不确定我的解释是否适合所有情况,但我怀疑您的计算机具有非常大的缓存,然后您不会遇到很多缓存错误。跨度>
  • 这是一个非常合理的解释,我完全同意,只是理论上编译器甚至可以优化数组语法版本并折叠循环。不过,根据我的经验,这不太可能。
【解决方案2】:

我不知道编译器到底做了什么,所以不是一个真正的答案,而是太多的文字评论...... 我怀疑编译器将数组表示法扩展为如下内容:

do j = 1, mSize
    do i = 1, mSize
        c(i, j, 1) = a(i, j) + b(i, j)
    end do
end do
do j = 1, mSize
    do i = 1, mSize
        c(i, j, 2) = a(i, j) - b(i, j)
    end do
end do
do j = 1, mSize
    do i = 1, mSize
        c(i, j, 3) = a(i, j) * b(i, j)
    end do
end do

当然,如果这样写,编译器可能仍然会折叠这些循环,所以你可能需要让他更迷惑,例如在循环之间写一些 c 到屏幕上。

【讨论】:

  • 使用 ifort14 -O3,冒号和循环版本的运行速度似乎都一样快,因此编译器可能已将上述三个循环融合为一个循环。我还尝试通过在第一个和第二个循环之间插入 print *、c(1,1,1) 等来“作弊”,然后速度显着下降...... :)
猜你喜欢
  • 2021-10-04
  • 2019-11-24
  • 2016-10-09
  • 1970-01-01
  • 2018-09-11
  • 1970-01-01
  • 1970-01-01
  • 2014-10-31
  • 1970-01-01
相关资源
最近更新 更多