我们需要预分配。但是MATLAB不会预分配预分配吗?

时间:2018-08-23 14:13:21

标签: matlab memory-management ram

在测试any()是否短路时(确实如此!),当preallocating测试变量时,我发现了以下有趣的行为:

test=zeros(1e7,1);
>> tic;any(test);toc
Elapsed time is 2.444690 seconds.
>> test(2)=1;
>> tic;any(test);toc
Elapsed time is 0.000034 seconds.

但是,如果我这样做:

test=ones(1e7,1);
test(1:end)=0;
tic;any(test);toc
Elapsed time is 0.642413 seconds.
>> test(2)=1;
>> tic;any(test);toc
Elapsed time is 0.000021 seconds.

事实证明,发生这种情况是因为变量直到真正被信息完全填充才真正在RAM上,因此第一个测试花费了更长的时间,因为它需要分配它。我检查此问题的方法是查看Windows任务管理器中使用的内存。

虽然这可能是有道理的(直到需要时才进行初始化),但令我更加困惑的是以下测试,该测试将变量填充在for循环中,并在某个时刻停止了执行。

test=zeros(1e7,1);

for ii=1:1e7
    test(ii)=1;
    if ii==1e7/2
        pause
    end
end

在检查MATLAB所使用的内存时,我可以看到停止时如何使用它,它仅使用test所需内存的50%(如果已满)。可以用不同的内存百分比可靠地重现此内容。

有趣的是,以下内容也不分配整个矩阵。

test=zeros(1e7,1);
test(end)=1;

我知道MATLAB不会在循环中动态分配test并增加其大小,因为这将使最终迭代非常慢(由于需要大量的内存拷贝),并且还会分配我提出的最后一个测试中的整个阵列。所以我的问题是:

发生了什么事?

有人建议这可能与虚拟内存与物理内存有关,并且与操作系统如何看待内存有关。虽然不确定如何链接到此处提出的第一个测试。任何进一步的解释都是理想的。

Win 10 x64,MATLAB 2017a

1 个答案:

答案 0 :(得分:11)

此行为并非MATLAB独有。实际上,MATLAB无法控制它,因为Windows是导致它的原因。 Linux和MacOS表现出相同的行为。

很多年前,我在C程序中注意到了同样的事情。事实证明,这是有据可查的行为。 This excellent answer详细介绍了内存管理在大多数现代OS中的工作原理(感谢Amro共享链接!)。如果此答案对您来说不够详细,请仔细阅读。

首先,让我们在C中重复Ander的实验:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main (void) {

   const int size = 1e8;

   /* For Linux: */
   // const char* ps_command = "ps --no-headers --format \"rss vsz\" -C so";
   /* For MacOS: */
   char ps_command[128];
   sprintf(ps_command, "ps -o rss,vsz -p %d", getpid());

   puts("At program start:");
   system(ps_command);

   /* Allocate large chunck of memory */

   char* mem = malloc(size);

   puts("After malloc:");
   system(ps_command);

   for(int ii = 0; ii < size/2; ++ii) {
      mem[ii] = 0;
   }

   puts("After writing to half the array:");
   system(ps_command);

   for(int ii = size/2; ii < size; ++ii) {
      mem[ii] = 0;
   }

   puts("After writing to the whole array:");
   system(ps_command);

   char* mem2 = calloc(size, 1);

   puts("After calloc:");
   system(ps_command);

   free(mem);
   free(mem2);
}

上面的代码可在POSIX兼容的操作系统(Windows以外的任何操作系统)上运行,但是在Windows上,您可以使用Cygwin成为POSIX兼容的(大部分)。您可能需要根据操作系统来更改ps命令的语法。用gcc so.c -o so编译,用./so运行。我在MacOS上看到以下输出:

At program start:
   RSS      VSZ
   800  4267728
After malloc:
   RSS      VSZ
   816  4366416
After writing to half the array:
   RSS      VSZ
 49648  4366416
After writing to the whole array:
   RSS      VSZ
 98476  4366416
After calloc:
   RSS      VSZ
 98476  4464076

显示两列,RSS和VSZ。 RSS代表“居民集大小”,它是程序使用的物理内存(RAM)的数量。 VSZ代表“虚拟大小”,它是分配给程序的虚拟内存的大小。两种数量都在KiB中。

VSZ列在程序启动时显示4 GiB。我不确定这到底是什么,这似乎是最重要的。但是该值在malloc之后和calloc之后再次增大,两次都约为98,000 KiB(略微超过我们分配的1e8字节)。

相反,在分配1e8字节后,RSS列显示仅增加了16 KiB。写完一半数组后,我们使用了超过5e7字节的内存,写完整个数组后,我们使用了超过1e8字节的内存。因此,内存是在使用时分配的,而不是在第一次请求时分配的。接下来,我们使用calloc分配另一个1e8字节,并且RSS中没有变化。请注意,calloc返回一个初始化为0的内存块,就像MATLAB的zeros一样。

我说的是calloc,因为MATLAB的zeros可能是通过calloc实现的。

说明:

现代计算机体系结构将虚拟内存(进程看到的内存空间)与物理内存分开。进程(即程序)使用指针访问内存,这些指针是虚拟内存中的地址。系统会将这些地址转换为 使用时的物理地址 。这具有许多优点,例如,一个进程不可能寻址分配给另一进程的内存,因为它无法生成的地址都不会转换为未分配给该进程的物理内存。它还允许OS换出空闲进程的内存,让另一个进程使用该物理内存。请注意,虚拟内存连续块的物理内存不必是连续的!

关键是上面的粗体斜体文本: 使用时 。分配给进程的内存实际上可能不存在,直到该进程尝试对其进行读取或写入。这就是为什么我们在分配大型数组时看不到RSS的任何变化的原因。使用的内存以页为单位分配给物理内存(块通常为4 KiB,有时可达1 MiB)。因此,当我们写入新内存块的一个字节时,只会分配一页。

某些操作系统(例如Linux)甚至会“过量使用”内存。在假定那些进程不会使用它们分配的所有内存的前提下,Linux将为其分配的虚拟内存多于它可以放入物理内存的容量。 This answer会告诉您过多的过度使用,而不是您想知道的。

那么calloc会返回零初始化的内存吗? the answer I linked earlier中也对此进行了说明。对于小型数组malloccalloc,从程序启动时从操作系统获得的较大池中返回一块内存。在这种情况下,calloc将向所有字节写入零,以确保将其初始化为零。但是对于较大的阵列,可以直接从OS获得新的内存块。操作系统总是提供被清零的内存(同样,它阻止一个程序查看另一程序的数据)。但是,因为直到使用完才对内存进行物理分配,所以归零也会延迟到将内存页放入物理内存中为止。

返回MATLAB:

以上实验表明,可以在恒定时间内获得调零的内存块,而无需更改程序内存的物理大小。这就是MATLAB的函数zeros分配内存的方式,而您却看不到MATLAB的内存占用量有任何变化。

实验还显示zeros分配了整个数组(可能通过calloc),并且内存占用量仅随着使用该数组而增加,一次一页。

The preallocation advice by the MathWorks指出

  

您可以通过预分配数组所需的最大空间来缩短代码执行时间。

如果我们分配一个小数组,然后要增加其大小,则必须分配一个新的数组并复制数据。数组与RAM的关联方式对此没有影响,MATLAB仅看到虚拟内存,它无法控制(甚至不知道?)这些数据在物理内存(RAM)中的存储位置。从MATLAB的角度(或任何其他程序的角度)而言,对于数组而言,重要的是该数组是虚拟内存的连续块。并非总是(通常不是?)不可能扩大现有的内存块,因此将获得新的内存块并复制数据。例如,请参见the graph in this other answer:当数组放大时(在较大的垂直尖峰处发生),将复制数据;否则,将复制数据。数组越大,需要复制的数据就越多。

预分配避免扩大数组,因为我们将其扩大到足够大。实际上,制作一个对我们需要的东西来说太大的数组会更有效,因为实际上我们从未真正将未使用的数组部分从未提供给程序。也就是说,如果我们分配很大的虚拟内存块,并且仅使用前1000个元素,那么我们实际上只会使用几页物理内存。

上述calloc的行为也可以解释this other strange behavior of the zeros function:对于小型数组,zeros的价格比大型数组贵,这是因为程序需要将小型数组显式归零,而大型数组会被操作系统隐式归零。