使用输入文件设置的常量标志优化科学代码

Optimisation of scientific code with constant flags set by input file

防止在 运行 期间不更改但必须在 运行 时间指定的逻辑值重复条件评估的最佳方法是什么?

该应用程序是科学计算,涉及读取一系列输入的大型代码。 然后代码 运行s 使用这些相同的输入值持续数天、数周甚至数月。 其中一些输入是开启某些功能或调整计算方法的标志。 一个例子是:

do i = 1, N
do j = 1, M

    !Some calculation
    calculated_value = ...

    !Flags specify how to use or adjust the calculated_value
    if (flag1) then
        calculated_value = calculated_value  + 1
    endif

    if (flag2) then
        call save_value(calculated_value)
    endif

    if (flag3) ...

end do
end do

标志必须在循环内,因为它们打开的功能使用在循环内获得的数据。 但是,必须在每个循环中评估标志,并且随着标志数量的增加,效率会越来越低。 我正在考虑的一些可能的解决方案包括:

我记得听说条件语句通常被假定为其先前的值,并且仅在最后执行检查。 在这种情况下,也许使用固定标志与效率无关。 这一定是数值计算中的一个常见问题,但我在 google.

上找不到好的 discussion/solution

编辑:添加代码来计时无标志、参数标志、变量标志和@Alexander Vogt 标志以定义例程的选择。

!Module of all permatations of flag conditions
module all_variants

contains

subroutine loop_Flag1_Flag2_Flag3(M,N,a,rand)
    implicit none
    integer, intent(in) :: M, N
    double precision, dimension(:),allocatable, intent(in)   :: rand
    double precision, intent(inout)   :: a

    integer    :: i,j

#define COND_FLAG1
#define COND_FLAG2
#define COND_FLAG3

#include "common_code.inc.F90"

end subroutine loop_Flag1_Flag2_Flag3

subroutine loop_Flag1_Flag2_nFlag3(M,N,a,rand)
    implicit none
    integer, intent(in) :: M, N
    double precision, dimension(:),allocatable, intent(in)   :: rand
    double precision, intent(inout)   :: a

    integer    :: i,j

#define COND_FLAG1
#define COND_FLAG2
#ifdef COND_FLAG3
#undef COND_FLAG3
#endif

#include "common_code.inc.F90"

end subroutine loop_Flag1_Flag2_nFlag3

subroutine loop_Flag1_nFlag2_nFlag3(M,N,a,rand)
    implicit none
    integer, intent(in) :: M, N
    double precision, dimension(:),allocatable, intent(in)   :: rand
    double precision, intent(inout)   :: a

    integer    :: i,j

#define COND_FLAG1
#ifdef COND_FLAG2
#undef COND_FLAG2
#endif
#ifdef COND_FLAG3
#undef COND_FLAG3
#endif

#include "common_code.inc.F90"

end subroutine loop_Flag1_nFlag2_nFlag3

subroutine loop_nFlag1_nFlag2_nFlag3(M,N,a,rand)
    implicit none
    integer, intent(in) :: M, N
    double precision, dimension(:),allocatable, intent(in)   :: rand
    double precision, intent(inout)   :: a

    integer    :: i,j

#ifdef COND_FLAG1
#undef COND_FLAG1
#endif
#ifdef COND_FLAG2
#undef COND_FLAG2
#endif
#ifdef COND_FLAG3
#undef COND_FLAG3
#endif

#include "common_code.inc.F90"

end subroutine loop_nFlag1_nFlag2_nFlag3

end module all_variants

!Some generic subroutine
subroutine write_a(a)
    implicit none

    double precision,intent(in) :: a

    print*, a

end subroutine write_a

!Main program to time various flag options

program optimise_flags
    use all_variants
    implicit none

    logical             :: flag1, flag2, flag3
    logical,parameter   :: pflag1 = .false., pflag2=.false., pflag3=.false.
    integer             :: i,j, N,M, rep, repeats
    double precision    :: a, t1,t2
    double precision    :: tnf, tpf, tvf, tppf
    double precision    :: anf, apf, avf, appf
    double precision, dimension(:),allocatable   :: rand

    !Number of runs and zero counters
    N = 1000; M = 1000; repeats = 1000
    allocate(rand(N*M))
    tnf = 0.d0; tpf = 0.d0; tvf = 0.d0; tppf = 0.d0
    anf = 0.d0; apf = 0.d0; avf = 0.d0; appf = 0.d0

    !Setup variable inputs
    open(unit=10,file='./input')
    read(10,*) flag1
    read(10,*) flag2
    read(10,*) flag3
    close(unit=10,status='keep')

    !Main loop
    do rep = 1, repeats

        !Generate array of random numbers
        !call reset_seed()
        call random_number(rand(:))

        !vvvvvvv Run with no flags vvvvvv
        a = 0.d0
        call cpu_time(t1)
        do i = 1,N
        do j = 1,M
            a = a + rand(j+(i-1)*M)
        enddo
        enddo
        call cpu_time(t2)
        anf = anf + a
        tnf = tnf + t2-t1
        !^^^^^^^ Run with no flags ^^^^^^

        !vvvvvvv Run with parameter flags vvvvvv
        a = 0.d0
        call cpu_time(t1)
        do i = 1,N
        do j = 1,M
            a = a + rand(j+(i-1)*M)

            if (pflag1) a = a + 1.d0
            if (pflag2) call write_a(a)
            if (pflag3) a = a**3.d0
        enddo
        enddo
        call cpu_time(t2)
        apf = apf + a
        tpf = tpf + t2-t1
        !^^^^^^^ Run with parameter flags ^^^^^^

        !vvvvvvv Run with variable input flags vvvvvvv
        a = 0.d0
        call cpu_time(t1)
        do i = 1,N
        do j = 1,M
            a = a + rand(j+(i-1)*M)

            if (flag1) a = a + 1.d0
            if (flag2) call write_a(a)
            if (flag3) a = a**3.d0
        enddo
        enddo
        call cpu_time(t2)
        avf = avf + a
        tvf = tvf + t2-t1
        ! ^^^^^^ Run with variable input flags  ^^^^^^

        ! vvvvvvv Run with copied subroutines flags vvvvvvv
        a = 0.d0
        call cpu_time(t1)
        !Choose a subroutine using pre-defined flags
        if ( flag1 ) then
          if ( flag2 ) then
            if ( flag3 ) then
              call loop_Flag1_Flag2_Flag3(M,N,a,rand)
            else
              call loop_Flag1_Flag2_nFlag3(M,N,a,rand)
            endif
          else
              call loop_Flag1_nFlag2_nFlag3(M,N,a,rand)
          endif
        else
            call loop_nFlag1_nFlag2_nFlag3(M,N,a,rand)
        endif
        call cpu_time(t2)
        appf = appf + a
        tppf = tppf + t2-t1
        ! ^^^^^^^ Run with copied subroutines flags ^^^^^^^

    enddo

    print'(4(a,e14.7))', 'Results: for no flag = ', anf,  ' Param flag = ', apf, ' Variable flag = ', avf, ' Pre-proc =', appf
    print'(4(a,f14.7))', 'Timings: for no flag = ', tnf,  ' Param flag = ', tpf, ' Variable flag = ', tvf, ' Pre-proc =', tppf

end program optimise_flags

输入文件包含:

.false.
.false.
.false.

我的计时结果因优化标志和编译器而异,通常是: 对于 ifort -fpp -O3 -xHost -ipo -fast optimise_flags.f90

对于gfortran -cpp -O3 optimise_flags.f90

结论是使用变量标志确实会导致性能下降,@Alexander Vogt 提出的解决方案有效。

据我所知,这些标志一个问题,尤其是当编译器不能轻松优化它们时。如果性能至关重要,我最好的猜测是将子程序分开。下面我勾画了一个方案,你可以如何在没有代码重复的情况下实现它。这是否会加速您的代码取决于实际代码以及循环和条件的复杂性,因此您需要尝试一下看看它是否真的值得付出努力。

您可以使用 #include 有效地实现您提到的最后一个选项(单独的(但几乎相同的)子例程)以避免代码重复:

common_code.inc.F90:

do i = 1, N
do j = 1, M

    !Some calculation
    calculated_value = ...

    !Flags specify how to use or adjust the calculated_value
    #ifdef COND_FLAG1
        calculated_value = calculated_value  + 1
    #endif

    #ifdef COND_FLAG2
        call save_value(calculated_value)
    #endif

    #ifdef COND_FLAG3
    !...
    #endif

end do
end do

个别子程序:

module all_variants

contains

  subroutine loop_Flag1_nFlag2_nFlag3()
    ! ...
    #define COND_FLAG1

    #ifdef COND_FLAG2
    #undef COND_FLAG2
    #endif

    #ifdef COND_FLAG3
    #undef COND_FLAG3
    #endif

    #include "common_code.inc.F90"
  end subroutine

  ! ...
end module

然后你需要处理所有情况:

if ( flag1 ) then
  if ( flag2 ) then
    if ( flag3 ) then
      call loop_Flag1_Flag2_Flag3()
    else
      call loop_Flag1_Flag2_nFlag3()
    endif
  else
    ! ...
  endif
else
  ! ...
endif