在测试是否有任何()短路(它确实!)时,我在预分配测试变量时发现了以下有趣的行为:
测试=零(1e7,1);>>抽动;任何(测试); TOC经过的时间是2 ….
这种行为并不是MATLAB独有的。事实上,MATLAB无法控制它,因为它是导致它的Windows。 Linux和MacOS显示相同的行为。
多年前我在C程序中注意到了这一点。事实证明,这是记录良好的行为。 这个优秀的答案 详细解释了内存管理在大多数现代操作系统中的工作原理(谢谢 荷银 分享链接!)。如果这个答案没有足够的细节,请阅读它。
首先,让我们重复Ander在C中的实验:
#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上看到以下输出:
ps
gcc so.c -o so
./so
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代表“虚拟大小”,它是分配给程序的虚拟内存的大小。两个数量都是KiB。
VSZ列在程序启动时显示4 GiB。我不确定那是什么,它似乎超过顶部。但价值增长之后 malloc 之后又一次 calloc ,两次都有大约98,000 KiB(略高于我们分配的1e8字节)。
malloc
calloc
相反,在我们分配1e8字节后,RSS列显示仅增加了16 KiB。写入数组的一半后,我们使用了超过5e7字节的内存,写入完整数组后,我们使用了超过1e8字节。因此,当我们使用它时,内存会被分配,而不是在我们第一次请求它时。接下来,我们使用分配另外的1e8字节 calloc ,并看到RSS没有变化。注意 calloc 返回一个初始化为0的内存块,与MATLAB完全相同 zeros 确实。
zeros
我在说 calloc 因为很可能是MATLAB的 zeros 通过实施 calloc 。
的 说明: 强>
现代计算机架构分开 的 虚拟内存 强> (进程看到的内存空间)来自 的 物理内存 强> 。该过程(即程序)使用指针访问存储器,这些指针是虚拟存储器中的地址。这些地址由系统转换为物理地址 的 使用时 强> 。这具有许多优点,例如,一个进程不可能寻址分配给另一个进程的存储器,因为它可以生成的地址都不会被转换为未分配给该进程的物理存储器。它还允许操作系统交换空闲进程的内存,让另一个进程使用该物理内存。请注意,连续的虚拟内存块的物理内存不需要是连续的!
关键是上面的粗体斜体文字: 的 使用时 强> 。分配给进程的内存可能实际上不存在,直到进程尝试读取或写入进程。这就是我们在分配大型数组时看不到RSS的任何变化的原因。使用的内存分配给页面中的物理内存(块通常为4 KiB,有时最高为1 MiB)。因此,当我们写入新内存块的一个字节时,只分配一个页面。
有些操作系统,比如Linux,甚至会“过度使用”内存。 Linux将为进程分配更多的虚拟内存,而不是它具有放入物理内存的能力,假设这些进程不会使用它们分配的所有内存。 这个答案 会告诉你更多关于过度使用而不是你想知道的事情。
那么会发生什么 calloc ,返回零初始化内存?这也在解释中 我之前联系的答案 。适用于小型阵列 malloc 和 calloc 从一个较大的池中返回一块内存,该池从OS开始时获得程序。在这种情况下, calloc 将零写入所有字节以确保它是零初始化。但对于较大的阵列,可直接从OS获取新的内存块。操作系统总是给出被清零的内存(同样,它会阻止一个程序查看来自另一个程序的数据)。但是因为在使用之前内存没有被物理分配,所以归零也会延迟,直到将内存页放入物理内存。
的 回到MATLAB: 强>
上面的实验表明,可以在恒定时间内获得一个归零的内存块,而无需改变程序内存的物理大小。这就是MATLAB的功能 zeros 在没有看到MATLAB内存占用的任何变化的情况下分配内存。
实验也表明了这一点 zeros 分配完整的数组(可能通过 calloc ),并且内存占用量仅在使用此数组时增加,一次一页。
MathWorks的预分配建议 说明
您可以通过预分配阵列所需的最大空间来缩短代码执行时间。
如果我们分配一个小数组,然后想要增加它的大小,则必须分配一个新数组并复制数据。数组如何与RAM相关联对此没有影响,MATLAB只看到虚拟内存,它无法控制(甚至知道?)物理内存(RAM)中存储这些数据的位置。从MATLAB的角度(或任何其他程序的角度)来看,对于数组而言,重要的是该数组是一个连续的虚拟内存块。扩大现有的内存块并不总是(通常不是?),因此可以获得新的块并复制数据。例如,请参阅 另一个答案中的图表 :当数组被放大时(这发生在大的垂直尖峰处)数据被复制;数组越大,需要复制的数据就越多。
预分配避免了扩大阵列,因为我们使它足够大以便开始。事实上,制作一个对我们需要的太大的数组更有效率,因为我们不使用的数组部分实际上从未真正给予程序。也就是说,如果我们分配一个非常大的虚拟内存块,并且只使用前1000个元素,我们将只使用几页物理内存。
的行为 calloc 以上描述也解释了 这个其他奇怪的行为了 zeros 功能 :对于小型阵列, zeros 比大型数组更昂贵,因为小型数组需要由程序显式归零,而大型数组则由OS隐式归零。