如何选择二维数组 A(i,j) 的最佳配置

How to choose the best configuration of 2D array A(i,j)

希望你能解释一下这件事。我正在使用 Fortran 并实际编写关于 CFD 主题的代码,下面(只是为了简单起见,只是为了举例)是对我的案例的简短解释:

  1. 我应该使用一个二维数组 A(i,j) 和一个一维数组 B(i)
  2. 我要循环2次,第一次循环应该是50000次,第二次是5次(不能改)。
  3. 上面的第2点应该循环10000次。

我用两个版本编写代码(我称它们为 Prog_A 和 Prog_B)。

第一个如下图:

PROGRAM PROG_A
 REAL*8, DIMENSION(50000,5):: A
 REAL*8, DIMENSION(50000)::B  
 REAL*8:: TIME1,TIME2
       !Just for initial value
           DO I=1, 50000
              A(I,1)=1.0
              A(I,2)=2.0
              A(I,3)=3.0
              A(I,4)=4.0
              A(I,5)=5.0

              B(I)=I 
           END DO
       !Computing computer's running time (start)
           CALL CPU_TIME(TIME1)
                 DO K=1, 100000
                    DO I=1, 50000             !Array should be computed first for 50,000 elements (can't be changed)
                        DO J=1, 5
                           A(I,J)=A(I,J)+SQRT(B(I))   
                        END DO
                    END DO
                 END DO
       !Computing computer's running time (finish)
           CALL CPU_TIME(TIME2)
             PRINT *, 'Elapsed real time = ', TIME2-TIME1, 'second(s)'
END PROGRAM PROG_A

第二个是:

PROGRAM PROG_B
 REAL*8, DIMENSION(5,50000):: A
 REAL*8, DIMENSION(50000)::B  
 REAL*8:: TIME1,TIME2
       !Just for initial value
           DO J=1, 50000
              A(1,J)=1.0
              A(2,J)=2.0
              A(3,J)=3.0
              A(4,J)=4.0
              A(5,J)=5.0

              B(J)=J 
           END DO
       !Computing computer's running time (start)
           CALL CPU_TIME(TIME1)
                 DO K=1, 100000
                    DO J=1, 50000             !Array should be computed first for 50,000 elements (can't be changed)
                        DO I=1, 5
                           A(I,J)=A(I,J)+SQRT(B(J))   
                        END DO
                    END DO
                 END DO
       !Computing computer's running time (finish)
           CALL CPU_TIME(TIME2)
             PRINT *, 'Elapsed real time = ', TIME2-TIME1, 'second(s)'
END PROGRAM PROG_B

如您所见,不同之处在于第一个我使用了二维数组 A(50000,5),第二个我使用了二维数组 A(5,50000)。

据我所知,由于 Fortran 基于 "column major",因此第二种情况会比第一种情况更快,因为我(在第二种情况下)执行了数组最内侧的循环(在这种情况下,i=1, ..., 5)。

但在 gfortran 上编译后(使用 -O3 优化),我发现第二个甚至比第一个慢得多。这是结果:

  1. 第一种情况:经过时间 = 29.187 秒
  2. 第二种情况:经过时间 = 70.496 秒

谁能解释一下为什么?

PS:两种情况的结果肯定是一样的。

编译器可能会做这样的事情:

    DO K=1, 100000
       DO I=1, 50000
          tmp = sqrt(b(i))
          A(I,1) = A(I,1) + tmp
          A(I,2) = A(I,2) + tmp
          A(I,3) = A(I,3) + tmp
          A(I,4) = A(I,4) + tmp
          A(I,5) = A(I,5) + tmp
       END DO
    END DO

Prog_A 中,这为您提供了步幅为 1 的良好访问模式。如果您像 Prog_B 中那样更改索引的顺序,您将获得此代码的步幅为 5 .其效果取决于机器,但肯定比简单的 stride-1 访问差。

部分优化基于实施选择的启发式方法。 您不会得到完全确定性的解释。 当用允许完整数组操作的语言编写程序时,例如 Fortran,要点是: 如果您可以将某些内容表示为完整的数组/或向量操作,请执行此操作并让编译器为您生成循环。那样的话,唯一要做的就是用最有效的编译器进行测试并坚持下去。

请记住,它依赖于编译器,因此会随编译器而变化。 例如,如果我按原样使用您的程序(更改 k 迭代次数 从 100000 到 10000 以加速它,并平均每次运行 5 次), 这是我电脑上不同编译器的时间。 我没有时间为您检查新版本的编译器。

pgf90 14
A: 5.05 s
B: 0.18 s

gfortran 4.9
A: 1.05 s
B: 4.02 s

ifort 13
A: 2.01 s
B: 1.08 s

你可以看到 gfortran 告诉你 B 不好的地方,pgfortran 完全相反,完全吹嘘 A

的结果

现在,如果我矢量化让编译器完成这项工作。这里我只修改A 并消除 I 循环得到这个:

DO K=1, 10000
    DO J=1, 5
        A(I,J)=A(I,J)+SQRT(B(I))
    END DO
END DO

然后我们得到这个(只有程序A)

pgf90 14: 5.05 s

gfortran 4.9: 5.04

ifort 13: 2.02 s

pgfortran 和 ifort 是稳定的,而 gfortran 在第一种情况下利用了一个技巧,可能是 haraldkl 的建议(见因素 5)。当我们向量化时,技巧并不明显 gfortran 表现不佳。似乎ifort和pgfortran 只需重写循环即可获得正确的顺序。

如果我变得更聪明并且也能限制 K 环

    DO J=1, 5
        A(:,J)=A(:,J)+10000*SQRT(B(:)) ! this seems to be final result
    END DO

然后我们得到这个(只有程序A)

pgf90 14: 0.001

gfortran 4.9: 0.001

ifort 13: 0.001 s

所有编译器都变得等效,因为几乎没有什么可以优化的。 您会看到,只需使用数组操作即可优化所有内容。


更新 High Performance Mark 在评论中指出,如果编译器发现结果未被使用,它实际上可能会跳过所有计算,这在某些实现中可能会发生。这个答案中给出的结果说明了这种可能性,尽管我一开始并没有提到它。为了防止编译器完全跳过代码,我在计算后(计时后)打印了结果所有元素的总和;结果与小数点后的第 3 位数字相同,足以得到 ~372684326034.9146 (~10^12) 的结果。这足以确保编译器进行实际计算。我完全忘了在答案中提到它。