Fortran内存分配不报错,但程序在初始化时被OS杀死

Fortran memory allocation does not give an error, but the program is killed by OS at initialization

鉴于下面提供的最小工作示例,您知道为什么在内存分配步骤不会发生内存分配错误吗?正如我检查的那样,当我使用 valgrind 运行 代码,或将参数 source=0.0 添加到内存分配语句时,正如预期的那样,我遇到了内存分配错误。

更新:我用最少的工作示例重现了这个问题:

 program memory_test

  implicit none

  double precision, dimension(:,:,:,:), allocatable :: sensitivity
  double precision, allocatable :: sa(:)
  double precision, allocatable :: sa2(:)

  integer :: ierr,nnz
  integer :: nx,ny,nz,ndata

  nx = 50
  ny = 50
  nz = 100
  ndata = 1600

  allocate(sensitivity(nx,ny,nz,ndata),stat=ierr)

  sensitivity = 1.0

  nnz = 100000000

  !allocate(sa(nnz),source=dble(0.0),stat=ierr)
  allocate(sa(nnz),stat=ierr)
  if(ierr /= 0) print*, 'Memory error!'

  !allocate(sa2(nnz),source=dble(0.0),stat=ierr)
  allocate(sa2(nnz),stat=ierr)
  if(ierr /= 0) print*, 'Memory error!'

  print*, 'Start initialization'

  sa = 0.0
  sa2 = 0.0

  print*, 'End initialization'

end program memory_test

当我 运行 它时,我没有打印消息 'Memory error!',但有消息 'Start initialization',然后程序被 OS 终止。如果我将内存分配与 'source' 参数一起使用(如代码中所注释),那么我只会收到消息 'Memory error!'.

对于内存统计,'free' 命令给我这个输出:

             total       used       free     shared    buffers     cached
Mem:       8169952    3630284    4539668      46240       1684     124888
-/+ buffers/cache:    3503712    4666240
Swap:            0          0          0

扩展评论而不是答案:

在 Fortran 中初始化有特定的含义;它指的是在声明时设置变量的值。所以这个

real :: myvar = 0.0

正在初始化。而这些

real :: myvar
....
myvar = 0.0

不是。现在,也许与您报告的问题更相关的是,此声明

isensit%sa(:) = 0.0

将值 0.0 分配给 数组部分 isensit%sa(:) 的每个元素。这与我认为您要写的内容非常(一旦您习惯了)不同,即:

isensit%sa = 0.0

此版本将值 0.0 分配给 数组 isensit%sa 的每个元素。因为数组部分(即使包含数组的每个元素的部分)不是数组,所以 Fortran 编译器在处理赋值时可能会临时为该部分分配 space。当您考虑更通用的数组部分时,这可能是有意义的。

我不确定我是否理解为什么您认为 allocate 语句执行时未分配 space 但我建议您整理一下分配,然后再考虑一下。而且我猜想为数组部分临时分配 space,这将与数组本身消耗的 space 一样多,可能会使您的程序超出边缘并导致您报告的行为。

顺便说一下,你可以试试这个语句

allocate(isensit%sa(isensit%nnz),source=0.0,stat=ierr)

如果您的编译器是最新的,应该在一条语句中进行分配并设置数组中的值。

哦,还有一个完全没有必要的评论:更喜欢 use mpi(或 use mpi_mod 或任何您的安装更喜欢 include mpif.h。这将防止(许多)错误可能由不匹配引起根据要求调用 mpi 例程。例程的使用关联意味着编译器可以检查参数匹配,包含头文件则不会。

您正在查看 linux 使用的内存分配策略的行为。当您分配内存但尚未写入时,它仅包含在虚拟内存中(请注意,这也可能受到特定 Fortran 运行时库的影响,但我不确定)。此内存存在于您的进程虚拟地址 space 中,但它不受任何实际物理内存页面的支持。只有当你写入内存时,才会分配物理页,并且只够满足写入。

考虑以下程序:

program test
   implicit none
   real,allocatable :: array(:) 

   allocate(array(1000000000)) !4 gb array

   print *,'Check memory - 4 GB allocated'
   read *

   array(1:1000000) = 1.0

   print *,'Check memory - 4 MB assigned'
   read *

   array(1000000:100000000) = 2.0

   print *,'Check memory - 400 MB assigned'
   read *

   array = 5.0

   print *,'Check memory - 4 GB assigned'
   read *

end program

此程序分配 4 GB 内存,然后写入 4 MB 数组部分、396 MB 数组部分(总写入 = 400 MB),最后写入完整数组(总写入 = 4 GB)。程序在每次写入之间暂停,因此您可以查看内存使用情况。

分配后,第一次写入前:

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                           
29192 casey     20   0 3921188   1176   1052 S   0.0  0.0   0:00.00 fortranalloc

所有内存都是虚拟内存 (VIRT),只有一小部分由物理内存 (RES) 支持。

4 MB 写入后:

29192 casey     20   0 3921188   5992   1984 S   0.0  0.0   0:00.00 fortranalloc

396 MB 写入后:

29192 casey     20   0 3921188 392752   1984 S   0.0  1.6   0:00.18 fortranalloc

并在 4 GB 之后写入:

29192 casey     20   0 3921188 3.727g   1984 S  56.6 15.8   0:01.88 fortranalloc 

请注意,每次写入后驻留内存都会增加以满足写入。这表明实际的物理内存分配仅发生在写入时,而不仅仅是分配,因此正常的 allocate() 无法检测到错误。当您将 source 参数添加到 allocate 时,会发生写入,这会导致内存的完全物理分配,如果失败,则可以检测到错误。

您可能看到的是内存耗尽时调用的 linux OOM Killer。当发生这种情况时,OOM Killer 将使用一种算法来确定要杀死什么以释放内存,并且您的代码的行为使其很可能成为被杀死的候选者。当你的写入导致可以满足的物理分配时,你的进程正在被内核杀死。由于上面详述的行为,您会在写入时看到它(由赋值引起)但不会在分配时看到。

下面是三种调用方式的比较allocate():

program mem_test
    implicit none
    integer, allocatable :: a(:,:,:)
    integer ierr, n1, n2, L, method

    n1 = 250000 ; n2 = 1000    !! 1-GB subarray

    print *, "Input: method, L"
    read *, method, L

    select case ( method )
    case ( 1 )
        allocate( a( n1, n2, L ) )              !! request L-GB virtual mem
    case ( 2 )
        allocate( a( n1, n2, L ), stat=ierr )   !! request L-GB virtual mem
        if ( ierr /= 0 ) stop "Memory error!"
    case ( 3 )
        allocate( a( n1, n2, L ), source=0 )    !! request L-GB resident mem
    endselect

    print *, "allocate() passed (type any key)"
    read *
end

这里使用的机器是Linux(x86_64),物理内存为64GB,交换磁盘为64GB。 ulimit -v 显示 "unlimited"。在所有情况下 (method = 1,2,3),程序都会针对 L > ~ 120(即物理内存和交换内存的总和)引发错误。对于 method = 1,3,系统报错

Operating system error: Cannot allocate memory
Allocation would exceed memory limit

而对于 method = 2,stat=ierr 检测到错误。对于 L < 120 程序继续到 运行,其中 method = 2 开始写入大量的 0...无论如何,在这台机器上,允许的最大内存量allocate() 似乎受到物理 + 交换大小的限制(合理的结果),尽管 ulimit -v 显示虚拟内存不受限制。


下面是使用 ulimit -v 限制 allocate() 允许的最大内存量的另一个测试。本程序分配4GB数组,并赋值2GB。

program alloc_test
    implicit none
    real, allocatable :: a(:), b(:)
    integer ierr, n

    n = 500000000

    allocate( a( n ), stat=ierr )   !! requests 2GB virtual memory
    if ( ierr /= 0 ) stop "Memory error! (a)"

    allocate( b( n ), stat=ierr )   !! requests 2GB virtual memory
    if ( ierr /= 0 ) stop "Memory error! (b)"

    print *, "before assignment (type any key)"
    call system( "ps aux | grep a.out" )
    read *

    print *, "now writing values..."
    a(:) = 0.0    !! request 2GB resident memory                        

    print *, "after assignment (type any key)"
    call system( "ps aux | grep a.out" )
    read *
end

如果我直接执行 ./a.out,该程序 运行 不会在 allocate() 处停止。我们现在根据 this page

将虚拟内存限制为 1GB
$ ( ulimit -v 1000000 ; ./a.out )

然后我们有

STOP Memory error! (a)

如果我们将其限制为 2.2 GB

STOP Memory error! (b)

最后,如果我们将其设置为 >4GB,则分配开始

before assignment (type any key)
<username>    12380  0.0  0.0 3918048  652 pts/1    S+   07:59   0:00 ./a.out

now writing values...

after assignment (type any key)
<username>    12380 38.0  2.9 3918048 1953788 pts/1 S+   07:59   0:00 ./a.out

因此我们可以限制虚拟内存的数量(如有必要),这样 allocate( ..., stat=ierr ) 会引发针对过度分配的错误。