为什么带有来自 "use module" 语句的数组的子例程比具有本地大小数组的相同子例程提供更快的性能?
Why does a subroutine with an array from a "use module" statement give faster performance than the same subroutine a locally sized array?
与此相关 ,但我相信此示例可以更清楚地识别问题。
我有一些遗留代码,如下所示:
subroutine ID_OG(N, DETERM)
use variables, only: ID
implicit real (A-H,O-Z)
implicit integer(I-N)
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG
将 use variables, only: ID
替换为 real, dimension(N) :: ID
或 real, dimension(:), allocatable :: ID
会导致明显的性能损失。为什么是这样?这是预期的行为吗?我在想是不是和程序需要为本地数组ID
重复分配内存有关,而use
语句允许程序跳过内存分配步骤
在遗留代码中 ID
在 module variables
中,但仅在子例程 ID_OG
中使用。它没有在代码中的其他任何地方使用——它既不是输入也不是输出。对我来说,将 ID
从 module variables
中删除并在子例程中本地定义似乎是很好的编程习惯。但也许并非如此。
最小工作示例(MWE):
使用 gfortran 8.2.0
编译为 gfortran -O3 test.f95
MODULE variables
implicit none
real, dimension(:), allocatable :: ID
END MODULE variables
program test
use variables
implicit none
integer :: N
integer :: loop_max = 1e6
integer :: ii ! loop index
real :: DETERM
real :: t1, t2
real :: t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate
character(*), parameter :: format_header = '((A5, 1X), 20(A12,1X))'
character(*), parameter :: format_data = '((I5, 1X), 20(ES12.5, 1X))'
open(1, file = 'TimingSubroutines_ID.txt', status = 'unknown')
write(1,format_header) 'N', 't_Legacy', 't_header', 't_head_No_ID', 't_Leg_no_ID', &
& 't_allocate'
do N = 1, 100
allocate(ID(N))
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG(N, DETERM)
end do
call CPU_time(t2)
t_ID_OG = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_header(N, DETERM)
end do
call CPU_time(t2)
t_ID_header = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_header_no_ID(N, DETERM)
end do
call CPU_time(t2)
t_ID_no_ID = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG_no_ID(N, DETERM)
end do
call CPU_time(t2)
t_OG_no_ID = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG_allocate(N, DETERM)
end do
call CPU_time(t2)
t_allocate = t2 - t1
print*, N, DETERM
deallocate(ID)
write(1,format_data) N, t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate
end do
end program test
subroutine ID_OG(N, DETERM)
use variables, only: ID
implicit real (A-H,O-Z)
implicit integer(I-N)
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG
subroutine ID_header(N, DETERM)
use variables, only: ID
implicit none
integer, intent(in) :: N
real, intent(out) :: DETERM
integer :: I
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_header
subroutine ID_header_no_ID(N, DETERM)
implicit none
integer, intent(in) :: N
real, intent(out) :: DETERM
integer :: I
real, dimension(N) :: ID
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_header_no_ID
subroutine ID_OG_no_ID(N, DETERM)
implicit real (A-H,O-Z)
implicit integer(I-N)
real, dimension(N) :: ID
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG_no_ID
subroutine ID_OG_allocate(N, DETERM)
implicit real (A-H,O-Z)
implicit integer(I-N)
real, dimension(:), allocatable :: ID
allocate(ID(N))
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG_allocate
分配数组需要时间。编译器可以自由分配它想要的本地数组 where-ever,但通常可以通过 compiler-specific 标志进行调整。对 gfortran 使用 -fstack-arrays
强制本地数组堆叠。
在栈上分配只是改变栈指针,几乎是免费的。然而,在堆上分配更复杂,需要一些簿记。
有局部变量有序的情况,也有全局(模块)变量有序的情况。还可以使用本地保存的变量或作为某些对象的组件的变量。 如果没有看到相关代码的完整设计,就不能说哪个更好。
FWIW,与 -fstack-arrays
我看不出有什么区别,除非使用 allocate()
:
显式分配
显式 allocate
将始终使用堆。
没有-fstack-arrays
我确实看到了一些:
图表非常嘈杂,因为我的笔记本同时 运行 许多进程。
这并不是说一定要用-fstack-arrays
,我是用来演示区别的。该选项很有用,但必须注意避免堆栈溢出错误。 -fmax-stack-var-size
可能会有所帮助。
正如您的测试所指出的,所有不使用 module
变量的方法的额外开销是由于该语言的哲学不会过多地打扰用户处理内存。
编译器将决定应该在何处分配内存,除非您开始修改编译器标志。您将 allocation/freeing 时间视为缺点,但您的分析还显示:
堆栈与堆内存处理开销迅速变得越来越小:对于 N>=100
,它已经 <50%。 dimension(100)
数组在现代计算机上是一个可笑的小内存块。
在模块中声明一个变量只是为了加速存储是一种 Fortran 90
使其成为全局变量的方式,因此,它是一种已弃用的编码风格。
我认为使代码 well-coded 和 快速的最佳策略是:
N
会在整个运行时保持不变吗?然后,将它封装成一个 class: 是个好主意
module myCalculation
implicit none
type, public: fancyMethod
integer :: N = 0
real, allocatable :: ID(:)
contains
procedure :: init
procedure :: compute
procedure :: is_init
end type fancyMethod
contains
elemental subroutine init(self,n)
class(fancyMethod), intent(inout) :: self
integer, intent(in) :: n
real, allocatable :: tmp(:)
self%N = n
allocate(tmp(N)); tmp(:) = 0
call move_alloc(from=tmp,to=self%ID)
end subroutine init
elemental logical function is_init(self)
class(fancyMethod), intent(in) :: self
is_init = allocated(self%ID) .and. size(self%ID)>0
end function is_init
real function compute(self,n,...) result(DETERM)
class(fancyMethod), intent(inout) :: self
integer, intent(in) :: n
....
if (.not.is_init(self)) call init(self,N)
DETERM = sum(self%ID(1:N))
end function compute
end module myCalculation
N
会保持不变吗?为什么不直接使用 PARAMETER
来定义它的最大尺寸呢?如果它是一个参数,编译器可能总是将自动数组放在堆栈上:
real function computeWithMaxSize(N) result(DETERM)
integer, intent(in) :: N
integer, parameter :: MAX_SIZE = 1024
real :: ID(MAX_SIZE)
[...]
if (N>MAX_SIZE) stop ' N is too large! '
DETERM = sum(ID(1:N))
end function computeWithMaxSize
N
会变得 variable-sized 和 大吗?然后,in-routine 内存处理很好,它的开销可能可以忽略不计,因为 CPU 时间将由计算支配;如果您不确定大小是否会大到导致任何堆栈问题,请使用 allocatable
版本:
real function computeWithAllocatable(N) result(DETERM)
integer, intent(in) :: N
real, allocatable :: ID(:)
allocate(ID(N))
[...]
DETERM = sum(ID(1:N))
end function computeWithAllocatable
与此相关
我有一些遗留代码,如下所示:
subroutine ID_OG(N, DETERM)
use variables, only: ID
implicit real (A-H,O-Z)
implicit integer(I-N)
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG
将 use variables, only: ID
替换为 real, dimension(N) :: ID
或 real, dimension(:), allocatable :: ID
会导致明显的性能损失。为什么是这样?这是预期的行为吗?我在想是不是和程序需要为本地数组ID
重复分配内存有关,而use
语句允许程序跳过内存分配步骤
在遗留代码中 ID
在 module variables
中,但仅在子例程 ID_OG
中使用。它没有在代码中的其他任何地方使用——它既不是输入也不是输出。对我来说,将 ID
从 module variables
中删除并在子例程中本地定义似乎是很好的编程习惯。但也许并非如此。
最小工作示例(MWE): 使用 gfortran 8.2.0
编译为gfortran -O3 test.f95
MODULE variables
implicit none
real, dimension(:), allocatable :: ID
END MODULE variables
program test
use variables
implicit none
integer :: N
integer :: loop_max = 1e6
integer :: ii ! loop index
real :: DETERM
real :: t1, t2
real :: t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate
character(*), parameter :: format_header = '((A5, 1X), 20(A12,1X))'
character(*), parameter :: format_data = '((I5, 1X), 20(ES12.5, 1X))'
open(1, file = 'TimingSubroutines_ID.txt', status = 'unknown')
write(1,format_header) 'N', 't_Legacy', 't_header', 't_head_No_ID', 't_Leg_no_ID', &
& 't_allocate'
do N = 1, 100
allocate(ID(N))
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG(N, DETERM)
end do
call CPU_time(t2)
t_ID_OG = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_header(N, DETERM)
end do
call CPU_time(t2)
t_ID_header = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_header_no_ID(N, DETERM)
end do
call CPU_time(t2)
t_ID_no_ID = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG_no_ID(N, DETERM)
end do
call CPU_time(t2)
t_OG_no_ID = t2 - t1
print*, N, DETERM
call CPU_time(t1)
do ii = 1, loop_max
CALL ID_OG_allocate(N, DETERM)
end do
call CPU_time(t2)
t_allocate = t2 - t1
print*, N, DETERM
deallocate(ID)
write(1,format_data) N, t_ID_OG, t_ID_header, t_ID_no_ID, t_OG_no_ID, t_allocate
end do
end program test
subroutine ID_OG(N, DETERM)
use variables, only: ID
implicit real (A-H,O-Z)
implicit integer(I-N)
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG
subroutine ID_header(N, DETERM)
use variables, only: ID
implicit none
integer, intent(in) :: N
real, intent(out) :: DETERM
integer :: I
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_header
subroutine ID_header_no_ID(N, DETERM)
implicit none
integer, intent(in) :: N
real, intent(out) :: DETERM
integer :: I
real, dimension(N) :: ID
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_header_no_ID
subroutine ID_OG_no_ID(N, DETERM)
implicit real (A-H,O-Z)
implicit integer(I-N)
real, dimension(N) :: ID
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG_no_ID
subroutine ID_OG_allocate(N, DETERM)
implicit real (A-H,O-Z)
implicit integer(I-N)
real, dimension(:), allocatable :: ID
allocate(ID(N))
DETERM = 1.0
DO 1 I=1,N
1 ID(I)=0
DETERM = sum(ID)
end subroutine ID_OG_allocate
分配数组需要时间。编译器可以自由分配它想要的本地数组 where-ever,但通常可以通过 compiler-specific 标志进行调整。对 gfortran 使用 -fstack-arrays
强制本地数组堆叠。
在栈上分配只是改变栈指针,几乎是免费的。然而,在堆上分配更复杂,需要一些簿记。
有局部变量有序的情况,也有全局(模块)变量有序的情况。还可以使用本地保存的变量或作为某些对象的组件的变量。 如果没有看到相关代码的完整设计,就不能说哪个更好。
FWIW,与 -fstack-arrays
我看不出有什么区别,除非使用 allocate()
:
显式 allocate
将始终使用堆。
没有-fstack-arrays
我确实看到了一些:
图表非常嘈杂,因为我的笔记本同时 运行 许多进程。
这并不是说一定要用-fstack-arrays
,我是用来演示区别的。该选项很有用,但必须注意避免堆栈溢出错误。 -fmax-stack-var-size
可能会有所帮助。
正如您的测试所指出的,所有不使用 module
变量的方法的额外开销是由于该语言的哲学不会过多地打扰用户处理内存。
编译器将决定应该在何处分配内存,除非您开始修改编译器标志。您将 allocation/freeing 时间视为缺点,但您的分析还显示:
堆栈与堆内存处理开销迅速变得越来越小:对于
N>=100
,它已经 <50%。dimension(100)
数组在现代计算机上是一个可笑的小内存块。在模块中声明一个变量只是为了加速存储是一种
Fortran 90
使其成为全局变量的方式,因此,它是一种已弃用的编码风格。
我认为使代码 well-coded 和 快速的最佳策略是:
N
会在整个运行时保持不变吗?然后,将它封装成一个 class: 是个好主意
module myCalculation
implicit none
type, public: fancyMethod
integer :: N = 0
real, allocatable :: ID(:)
contains
procedure :: init
procedure :: compute
procedure :: is_init
end type fancyMethod
contains
elemental subroutine init(self,n)
class(fancyMethod), intent(inout) :: self
integer, intent(in) :: n
real, allocatable :: tmp(:)
self%N = n
allocate(tmp(N)); tmp(:) = 0
call move_alloc(from=tmp,to=self%ID)
end subroutine init
elemental logical function is_init(self)
class(fancyMethod), intent(in) :: self
is_init = allocated(self%ID) .and. size(self%ID)>0
end function is_init
real function compute(self,n,...) result(DETERM)
class(fancyMethod), intent(inout) :: self
integer, intent(in) :: n
....
if (.not.is_init(self)) call init(self,N)
DETERM = sum(self%ID(1:N))
end function compute
end module myCalculation
N
会保持不变吗?为什么不直接使用PARAMETER
来定义它的最大尺寸呢?如果它是一个参数,编译器可能总是将自动数组放在堆栈上:
real function computeWithMaxSize(N) result(DETERM)
integer, intent(in) :: N
integer, parameter :: MAX_SIZE = 1024
real :: ID(MAX_SIZE)
[...]
if (N>MAX_SIZE) stop ' N is too large! '
DETERM = sum(ID(1:N))
end function computeWithMaxSize
N
会变得 variable-sized 和 大吗?然后,in-routine 内存处理很好,它的开销可能可以忽略不计,因为 CPU 时间将由计算支配;如果您不确定大小是否会大到导致任何堆栈问题,请使用allocatable
版本:
real function computeWithAllocatable(N) result(DETERM)
integer, intent(in) :: N
real, allocatable :: ID(:)
allocate(ID(N))
[...]
DETERM = sum(ID(1:N))
end function computeWithAllocatable