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

Posted

技术标签:

【中文标题】我们需要预先分配。但是 MATLAB 没有预分配预分配?【英文标题】:We need to preallocate. But MATLAB does not preallocate the preallocation? 【发布时间】:2019-01-29 22:57:37 【问题描述】:

在测试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

【问题讨论】:

相关:***.com/q/19991623/7328782 链接的副本对发生的低杠杆“魔术”有非常详细的解释。这解释了在这篇文章中可以看到的一切。 @rahnema1 最终这是您需要了解的详细程度,但它不是一本书,而是另一个 SO 答案。如果我有时间的话,我会考虑总结一个简短的答案,描述为什么会发生这种情况。我编辑了代码,因为它在某些时候被错误地编辑了(由我) 【参考方案1】:

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

多年前,我在 C 程序中注意到了同样的事情。事实证明,这是有据可查的行为。 This excellent answer 详细解释了大多数现代操作系统中内存管理的工作原理(感谢 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 代表“Virtual size”,它是分配给程序的虚拟内存的大小。两个数量均以 KiB 为单位。

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

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

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

说明:

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

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

某些操作系统(如 Linux)甚至会“过度使用”内存。 Linux 将分配给进程的虚拟内存多于它可以放入物理内存的容量,前提是这些进程无论如何都不会使用分配给它们的所有内存。 This answer 会告诉你更多的过度使用,而不是你想知道的。

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

返回 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 比大数组更昂贵,因为小数组需要由程序显式归零,而大数组则隐式归零由操作系统。

【讨论】:

@Hadi:我不认为 MATLAB 会做这样的事情。我在这里描述的一切都在操作系统的控制之下。当 MATLAB 尝试使用 RAM 页面时,操作系统会将它们分配给 MATLAB。 MATLAB 不需要很聪明地使用内存,它可以分配整个数组并像在 RAM 中一样使用它。操作系统会在使用时将其部分放入 RAM。 @Hadi:MATLAB 为整个数组分配内存,但操作系统不会将任何 RAM 分配给 MATLAB,直到向其写入内容。这就是虚拟内存和物理内存的区别。正如你在我做的实验中看到的那样,当我调用malloc 时,虚拟内存大小增加了,但直到我将数据写入该数组时,物理内存大小才增加。 @LuisMendo:我已经编辑了答案的最后一部分,现在更清楚了吗? @Luis:似乎是这样。请注意,物理内存页面不需要是连续的,每个页面都可以位于 RAM 中的任何位置(或者如果换出,则位于硬盘驱动器上)。硬件负责将虚拟内存指针转换为物理内存位置。因此,如果您分配一个数组并写入第一个元素,您将获得分配的一页 RAM。当你写得更多时,将分配第二页 RAM,但第二页不需要紧挨着第一页。这使得操作系统可以轻松分配这些页面,无需移动内容以腾出空间。 @TomMozdzen 我认为您在谈论内存映射文件。您也可以在 MATLAB 中执行此操作,请参阅 the docs。也可以考虑tall arrays。

以上是关于我们需要预先分配。但是 MATLAB 没有预分配预分配?的主要内容,如果未能解决你的问题,请参考以下文章

matlab运行出现“变量似乎会随着迭代次数改变而变化,请预分配内存,以提高运行速度”问题

为啥 Matlab 警告我“不推荐预分配”

matlab矩阵内存预分配

预分配 unordered_map 的线程安全

有没有办法在 .NET 运行时预分配堆,比如 Java 中的 -Xmx/-Xms?

使用fallocate()在Linux中快速预分配大文件