内存泄漏与运行时多态性

huangapple go评论75阅读模式
英文:

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 4Invalid 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 4Invalid 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.

huangapple
  • 本文由 发表于 2023年7月10日 20:48:02
  • 转载请务必保留本文链接:https://go.coder-hub.com/76653898.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定