【问题标题】:array operation in fortranfortran中的数组操作
【发布时间】:2021-01-20 03:00:57
【问题描述】:

我正在编写一个包含大量二维数组并对其进行操作的代码。我希望代码尽可能简洁,因为我想在数组上使用尽可能多的“隐式”操作,但我真的不知道如何为二维数组编写它们。 例如:

DO  J=1,N
   DO  I=1,M
    A(I,J)=B(J)*A(I,J)
   ENDDO
ENDDO

变得容易:

DO  J=1,N
 A(:,J)=B(J)*A(:,J)
ENDDO

有没有办法减少循环J?

谢谢

【问题讨论】:

  • 不,不是,至少在我看来。我想你可以将二维数组打包成派生类型,并在该类型上定义一个合适的乘法运算,但是 IMO 上面的代码很好,简短明了,我看不出有什么问题。
  • 您也许可以在this other question 及其答案中找到一些想法,但我也很想保持原样。
  • 作为@francescalus 向我们指出的问题的答案之一的作者,我将重申:虽然在现代 Fortran 中制作单行代码来实现全数组操作很有趣,很少能写出与显式循环一样快的单行代码,而且几乎总是倾向于降低可读性。并且不要忘记,最有可能在 3 个月的时间内阅读代码并试图弄清楚“狡猾”的单行代码实际上做了什么的人是代码的作者。
  • 您可以将现有的两个 do 循环隐藏到一个单独的库函数中,并让函数的名称提供“简洁”的方面。在这种情况下可能是 COL_MULT?

标签: fortran


【解决方案1】:

为了简洁明了,您可以将这些操作包装在一个派生类型中。我写了一个不太简洁的最小示例,因为我需要初始化对象,但是一旦初始化完成,操作你的数组就变得非常简洁和优雅。

我在arrays_module.f90 中存储了一个派生类型arrays2d_T,它可以保存数组系数以及有用的信息(行数和列数)。此类型包含初始化过程以及您尝试执行的操作。

module arrays_module

implicit none

integer, parameter :: dp = kind(0.d0) !double precision definition

 type :: arrays2d_T
   real(kind=dp), allocatable :: dat(:,:)
   integer :: nRow, nCol     
  contains
   procedure :: kindOfMultiply => array_kindOfMuliply_vec
   procedure :: init           => initialize_with_an_allocatable
 end type

 contains

 subroutine initialize_with_an_allocatable(self, source_dat, nRow, nCol)
   class(arrays2d_t), intent(inOut) :: self
   real(kind=dp), allocatable, intent(in) :: source_dat(:,:)
   integer, intent(in) :: nRow, nCol
   allocate (self%dat(nRow, nCol), source=source_dat)
   self%nRow = nRow
   self%nCol = nCol
 end subroutine

 subroutine array_kindOfMuliply_vec(self, vec)
   class(arrays2d_t), intent(inOut) :: self
   real(kind=dp), allocatable, intent(in) :: vec(:)
   integer :: iRow, jCol
   do jCol = 1, self%nCol
   do iRow = 1, self%nRow
      self%dat(iRow, jCol) = vec(jCol)*self%dat(iRow, jCol)
   end do
   end do
 end subroutine

end module arrays_module

然后,在main.f90 中,我在一个简单的例子中检查了这个乘法的行为:

program main
use arrays_module
implicit none

type(arrays2d_T) :: A
real(kind=dp), allocatable :: B(:)

! auxilliary variables that are only useful for initialization
real(kind=dp), allocatable :: Aux_array(:,:)
integer :: M = 3
integer :: N = 2

! initialise the 2d array
allocate(Aux_array(M,N))
Aux_array(:,1) = [2._dp, -1.4_dp, 0.3_dp]
Aux_array(:,2) = [4._dp, -3.4_dp, 2.3_dp]
call A%init(aux_array, M, N)

! initialise vector
allocate (B(N))
B = [0.3_dp, -2._dp]

! compute the product
call A%kindOfMultiply(B)

print *, A%dat(:,1)
print *, A%dat(:,2)
end program main

编译可以像gfortran -c arrays_module.f90 && gfortran -c main.f90 && gfortran -o main.out main.o arrays_module.o一样简单

一旦存在这种面向对象的机制,call A%kindOfMultiply(B) 就会比 FORALL 方法更清晰(而且更不容易出错)。

【讨论】:

  • initialize_with_an_allocatable 的现代方式是写allocate (self%dat, source=source_dat) 或类似但语法正确的东西。上面的子程序并不是真正需要的。
  • 写@HighPerformanceMark 的allocate(self%dat, source=source_dat) 的现代方式是self%dat=source_dat。 (我实际上发现 do 结构比 call A%kindOfMultiply(B) 更清晰,但很高兴看到替代方案。)
  • 谢谢@HighPerformanceMark,我将您的建议添加到代码中。我曾经对这些现代分配语句有点担心,因为当我第一次尝试它们时,我的编译器不支持其中一些(不幸的是我不记得是哪一个)。我同意初始化子程序本质上替换了 1 行代码而不是 3 行。恕我直言,当主代码的复杂性增加时,它确实会有所不同:更容易关注抽象操作的流程,因为簿记是(如此轻微)减少。不过,我不能说这是品味问题。
  • @francescalus:嗯,是的,而且 d'ohhhh。
  • 在内部赋值时自动分配非多态数组是 F2003 的一项功能(对于多态,它是 F2008)并且现在得到了很好的支持(即使不是某些编译器的默认行为 - 对于最近的英特尔编译器,它是现在默认)。支持多态内在赋值确实比较麻烦,但是源分配也可以。
【解决方案2】:

使用FORALL可以实现单行解决方案:

FORALL(J=1:N) A(:,J) = B(J)*A(:,J)

请注意,FORALL 在最新版本的标准中已被弃用,但据我所知,这是您可以作为单行代码执行该操作的唯一方法。

【讨论】:

  • 是的。但由于它已被弃用,所以最好在任何新代码中完全避免。
  • Forall 大多是失败的,而且往往难以优化。
  • 基本上,如果你写DO J=1,N; A(:,J)=B(J)*A(:,J); ENDDO,它看起来几乎一样,而且也在一行上......可读性的差异是有争议的。
  • 感谢您的 cmets - 很高兴解决 FORALLDO CONCURRENT 或其他行为的问题:我一直在使用 gfortran 进行一些测试,但从未发现 FORALL 是任何较慢(也许我会将它们放在另一个线程中)。我显然站在历史的错误一边,将不得不屈服于所有这些冗长,但我真的很喜欢自 Fortran 90/95 以来 Fortran 开始实现的紧凑数组/矩阵语法:它本可以成为最快的编译语言,语法清晰,类似于 Matlab/Python。
【解决方案3】:

这里没有人提到do concurrent 构造,它有潜力自动并行化和加速你的代码,

do concurrent(j=1:n); A(:,j)=B(j)*A(:,j); end do

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-11-23
    • 1970-01-01
    • 2016-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-13
    相关资源
    最近更新 更多