我们需要预分配。但是MATLAB没有preallocate预分配?

We need to preallocate. But MATLAB does not preallocate the preallocation?

在测试 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 的大小,因为这会使结束迭代非常慢(由于需要大量的 memcopy),而且它也会在我建议的最后一个测试中分配整个数组。所以我的问题是:

这是怎么回事?

有人建议这可能与虚拟内存与物理内存有关,并且与 OS 如何看待内存有关。不过,不确定这与此处提出的第一个测试有何关联。任何进一步的解释都是理想的。

Win 10 x64,MATLAB 2017a

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

很多年前,我在 C 程序中注意到了这个完全相同的事情。事实证明,这是有据可查的行为。 This excellent answer explains in gory details how memory management works in most modern OSes (thanks Amro 分享 link!)。如果此答案对您来说不够详细,请阅读它。

首先,让我们在 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 兼容 OS(即除 Windows 之外的任何 OS),但在 Windows 上您可以使用 Cygwin 成为(大部分)POSIX 兼容。您可能需要根据您的 OS 更改 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 代表 "Resident set size",它是程序正在使用的物理内存 (RAM) 的数量。 VSZ代表"Virtual size",是分配给程序的虚拟内存大小。两个数量都以 KiB 为单位。

程序启动时 VSZ 列显示 4 GiB。我不确定那是什么,它似乎在顶部。但是值在 malloc 之后增长,在 calloc 之后再次增长,两次都约为 98,000 KiB(略高于我们分配的 1e8 字节)。

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

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

解释:

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

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

有些 OS ,例如 Linux,甚至 "overcommit" 内存。 Linux 将分配给进程的虚拟内存多于它能够放入物理内存的容量,假设这些进程无论如何都不会使用它们分配的所有内存。 This answer 会告诉你过度使用的信息比你想知道的要多。

那么 calloc 会发生什么,return 是零初始化内存? the answer I linked earlier 中也对此进行了解释。对于小数组 malloccalloc return 从程序开始时从 OS 获得的较大池中的内存块。在这种情况下,calloc 将向所有字节写入零以确保它是零初始化的。但是对于更大的数组,直接从OS中获取新的一块内存。 OS 总是给出清零的内存(同样,它防止一个程序看到另一个程序的数据)。但是因为内存在使用之前不会被物理分配,所以清零也会延迟到内存页被放入物理内存中。

返回 MATLAB:

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

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

The preallocation advice by the MathWorks 表示

you can improve code execution time by preallocating the maximum amount of space required for the array.

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

预分配避免扩大数组,因为我们使它足够大以开始。事实上,创建一个对于我们需要的来说太大的数组会更有效,因为我们不使用的数组部分实际上从未真正提供给程序。也就是说,如果我们分配一个非常大的虚拟内存块,并且只使用前 1000 个元素,那么我们实际上只会使用几页物理内存。

上述 calloc 的行为也解释了 this other strange behavior of the zeros function:对于小数组,zeros 比大数组更昂贵,因为小数组需要由程序,而大型数组由 OS.

隐式归零