英文:
Memory leak with runtime polymorphism
问题
在追踪我们代码的意外大内存消耗时,我认为我发现了gfortran的一个错误,我可以使用版本7.5.0、9.4.0和10.3.0来重现这个错误。这个错误在ifort 19.1.1.217中没有出现。
总结一下:
如果一个工厂函数返回一个可分配(而不是指针)的虚拟类实例,那么当它应该被正确销毁时,它显然没有被正确销毁。
这适用于以下情况:(1)结果在表达式中使用并应立即删除,或者(2)结果被分配给class(...), allocatable
类型的变量,并且该变量通过分配进行自动分配。
以下是演示问题的最小示例。
在我看来,所有这些示例都应该正常工作。所以我的问题有两个方面:
这实际上是符合标准的代码吗,还是因为我的编码错误而失败?如果这不起作用,我应该如何在实践中使用运行时多态性?
所有示例都使用以下模块文件:
module shapes_mod
implicit none
private
public :: Shape_t, Rectangle_t, Circle_t, PI, get_shape, get_volume
real, parameter :: PI = atan(1.0) * 4.0
type, abstract :: Shape_t
contains
procedure(get_area_t), deferred :: get_area
end type
abstract interface
elemental real function get_area_t(this)
import :: Shape_t
class(Shape_t), intent(in) :: this
end function
end interface
type, extends(Shape_t) :: Circle_t
real :: r
contains
procedure :: get_area => get_area_Circle_t
end type
type, extends(Shape_t) :: Rectangle_t
real :: a, b
contains
procedure :: get_area => get_area_Rectangle_t
end type
contains
elemental function get_area_Circle_t(this) result(res)
class(Circle_t), intent(in) :: this
real :: res
res = this%r**2 * PI
end function
elemental function get_area_Rectangle_t(this) result(res)
class(Rectangle_t), intent(in) :: this
real :: res
res = this%a * this%b
end function
pure function get_shape(arg1, arg2) result(res)
!! Contrived constructor, that gives a circle for one and a rectangle for two arguments.
real, intent(in) :: arg1
real, intent(in), optional :: arg2
class(Shape_t), allocatable :: res
if (present(arg2)) then
res = Rectangle_t(arg1, arg2)
else
res = Circle_t(arg1)
end if
end function
elemental function get_volume(base, h) result(res)
!! Get the volume of a prism of the 2D shape base and height h.
class(Shape_t), intent(in) :: base
real, intent(in) :: h
real :: res
res = h * base%get_area()
end function
end module
以下程序按预期正常工作:
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 4
real, allocatable :: volumes(:)
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
volumes(i) = get_volume(Rectangle_t(1., 2.), 5.)
else
volumes(i) = get_volume(Circle_t(2.), 5.)
end if
end do
write(*, *) volumes
end block
end program
以下程序使用了一个临时的class, allocatable
变量。
当使用valgrind
运行时,我得到Invalid write of size 4
和Invalid write of size 8
。
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 4
real, allocatable :: volumes(:)
class(Shape_t), allocatable :: shape
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
shape = Rectangle_t(1., 2.)
else
shape = Circle_t(3.)
end if
volumes(i) = get_volume(shape, 5.)
end do
write(*, *) volumes
end block
end program
下一个示例直接使用工厂函数的结果,而不进行赋值。
这个示例最接近我们大型代码中的实际问题。
它会导致内存泄漏,如果系统大小参数n
足够高,最终会耗尽内存(使用valgrind
确认)。
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 20
real, allocatable :: volumes(:)
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
volumes(i) = get_volume(get_shape(1., 2.), 5.)
else
volumes(i) = get_volume(get_shape(2.), 5.)
end if
end do
write(*, *) volumes
end block
end program
英文:
While tracking down unexpected large memory consumption of our code I think I found a bug in gfortran which I could reproduce with versions 7.5.0, 9.4.0, and 10.3.0.
The error does not appear in ifort 19.1.1.217.
In summary:
If a factory function returns an allocatable (not a pointer) instance of a virtual class, then it is apparently not correctly destroyed, when it should be.
This applies to cases, where (1) the result is used in an expression and should be immediately deleted, or (2) the result is assigned to a variable of class(...), allocatable
type and the variable is allocated via automatic allocation upon assignment.
The following minimal examples demonstrate the problem.
In my understanding all these examples should work. So my question is two-fold:
Is it actually standard-conforming code, or does it fail because of my coding error? How should I use runtime-polymorphism in practice if this does not work?
All examples use the following module file
module shapes_mod
implicit none
private
public :: Shape_t, Rectangle_t, Circle_t, PI, get_shape, get_volume
real, parameter :: PI = atan(1.0) * 4.0
type, abstract :: Shape_t
contains
procedure(get_area_t), deferred :: get_area
end type
abstract interface
elemental real function get_area_t(this)
import :: Shape_t
class(Shape_t), intent(in) :: this
end function
end interface
type, extends(Shape_t) :: Circle_t
real :: r
contains
procedure :: get_area => get_area_Circle_t
end type
type, extends(Shape_t) :: Rectangle_t
real :: a, b
contains
procedure :: get_area => get_area_Rectangle_t
end type
contains
elemental function get_area_Circle_t(this) result(res)
class(Circle_t), intent(in) :: this
real :: res
res = this%r**2 * PI
end function
elemental function get_area_Rectangle_t(this) result(res)
class(Rectangle_t), intent(in) :: this
real :: res
res = this%a * this%b
end function
pure function get_shape(arg1, arg2) result(res)
!! Contrived constructor, that gives a circle for one and a rectangle for two arguments.
real, intent(in) :: arg1
real, intent(in), optional :: arg2
class(Shape_t), allocatable :: res
if (present(arg2)) then
res = Rectangle_t(arg1, arg2)
else
res = Circle_t(arg1)
end if
end function
elemental function get_volume(base, h) result(res)
!! Get the volume of a prism of the 2D shape base and height h.
class(Shape_t), intent(in) :: base
real, intent(in) :: h
real :: res
res = h * base%get_area()
end function
end module
The following program works correctly as expected:
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 4
real, allocatable :: volumes(:)
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
volumes(i) = get_volume(Rectangle_t(1., 2.), 5.)
else
volumes(i) = get_volume(Circle_t(2.), 5.)
end if
end do
write(*, *) volumes
end block
end program
The following program uses a temporary class, allocatable
variable.
When running with valgrind
I get Invalid write of size 4
and Invalid write of size 8
.
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 4
real, allocatable :: volumes(:)
class(Shape_t), allocatable :: shape
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
shape = Rectangle_t(1., 2.)
else
shape = Circle_t(3.)
end if
volumes(i) = get_volume(shape, 5.)
end do
write(*, *) volumes
end block
end program
The next example uses the result of factory function directly without assignment.
This example is closest to our actual problem in our large code.
It does memory leak and if the system size parameter n
is high enough, one eventually runs out of memory (confirmed with valgrind
).
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 20
real, allocatable :: volumes(:)
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
volumes(i) = get_volume(get_shape(1., 2.), 5.)
else
volumes(i) = get_volume(get_shape(2.), 5.)
end if
end do
write(*, *) volumes
end block
end program
答案1
得分: 2
在玩了一段时间并纳入一些提供的评论后,我认为我解决了这个问题。
感谢@PierU、@Vladimir F Героям слава、@Ian Bush的评论!
评论中仍然缺少的是一个相当干净的解决方法。
因此,我现在将编写自己的答案。
如果有更好的答案,我愿意接受它们。
标准一致性
正如@PierU指出的,纯函数可能不会返回多态可分配的对象,纯子例程可能不会将它们作为intent(out)
参数。
这是一个有用的信息,但并没有解决问题。
事实上,这实际上是gfortran
中的一个错误,由@IanBush在这里发现。
自动释放
第一个例子,在其中valgrind
中出现Invalid write of size 4
和Invalid write of size 8
的问题,如果添加显式释放,则得以解决。
因此,它必须变成以下形式:
! ...
volumes(i) = get_volume(shape, 5.)
deallocate(shape)
工厂子例程
使用工厂子例程,并给shape
一个名称,而不是在表达式中重用结果,然后解决了问题。
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 20
real, allocatable :: volumes(:)
class(Shape_t), allocatable :: shape
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
call construct_shape(shape, 1., 2.)
else
call construct_shape(shape, 3.)
end if
volumes(i) = get_volume(shape, 5.)
end do
write(*, *) volumes
end block
contains
subroutine construct_shape(shape, arg1, arg2)
!! 虚构的构造函数,为一个参数提供一个圆形,为两个参数提供一个矩形。
class(Shape_t), allocatable, intent(out) :: shape
real, intent(in) :: arg1
real, intent(in), optional :: arg2
if (present(arg2)) then
shape = Rectangle_t(arg1, arg2)
else
shape = Circle_t(arg1)
end if
end subroutine
end program
请注意,在工厂子例程中,shape
具有intent(out), allocatable
属性,因此会自动释放。(这现在实际上在gfortran实现中有效。)
因此,不需要显式释放。
使用这段代码,它可以干净地通过valgrind。
英文:
After playing around for some time and incorporating some of the comments given, I think I solved the problem.
Thanks to @PierU, @Vladimir F Героям слава, @Ian Bush for their comments!
What was still missing from the comments was a reasonably clean workaround.
Hence I will write my own answer now.
If there are better ones, I am happy to accept them instead.
Standard conformance
As pointed out by @PierU, pure functions may not return polymorphic allocatables and pure subroutines may not have them as intent(out)
arguments. That is good to know, but did not fix the problem.
It is actually a bug in gfortran
as found by @IanBush here.
Automatic deallocation
The first example where one gets Invalid write of size 4
and Invalid write of size 8
in valgrind
is fixed, if one adds an explicit deallocation.
So it has to become
! ...
volumes(i) = get_volume(shape, 5.)
deallocate(shape)
Factory subroutine
Using a factory subroutine and giving the shape
a name instead of reusing the result in an expression solves then the problem.
program main
use shapes_mod, only: Shape_t, Rectangle_t, Circle_t, get_shape, get_volume
implicit none
block
integer :: i
integer, parameter :: n = 20
real, allocatable :: volumes(:)
class(Shape_t), allocatable :: shape
allocate(volumes(N))
do i = 1, n
if (mod(i, 2) == 0) then
call construct_shape(shape, 1., 2.)
else
call construct_shape(shape, 3.)
end if
volumes(i) = get_volume(shape, 5.)
end do
write(*, *) volumes
end block
contains
subroutine construct_shape(shape, arg1, arg2)
!! Contrived constructor, that gives a circle for one and a rectangle for two arguments.
class(Shape_t), allocatable, intent(out) :: shape
real, intent(in) :: arg1
real, intent(in), optional :: arg2
if (present(arg2)) then
shape = Rectangle_t(arg1, arg2)
else
shape = Circle_t(arg1)
end if
end subroutine
end program
Note that shape
is of intent(out), allocatable
in the factory subroutine, so it is automatically deallocated. (This now actually works in the gfortran implementation.)
So the explicit deallocation is not required.
With this code it cleanly passes valgrind.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论