我有一个用gcc 11.2编译的程序,它首先在堆上分配一些RAM内存(8GB((使用new(,然后用示波器实时读取的数据填充。
uint32_t* buffer = new uint32_t[0x80000000];
for(uint64_t i = 0; i < 0x80000000; ++i) buffer[i] = GetValueFromOscilloscope();
我面临的问题是优化器跳过了第一行的分配,并像遍历循环一样动态地进行分配。这会减慢循环每次迭代所花费的时间。因为在循环过程中尽可能高效是很重要的,我找到了一种方法来强制编译器在进入for循环之前分配内存,即将所有保留值设置为零:
uint32_t* buffer = new uint32_t[0x80000000]();
我的问题是:有没有一种侵入性较小的方法可以在不强制数据为零的情况下实现同样的效果(除了关闭优化标志(?我只是想强制编译器在声明时保留内存,但我不在乎保留值是否为零。
提前感谢!
第1版:我看到的关于优化器延迟分配的证据是,当我遍历循环时,"gnome系统监视器"显示RAM内存缓慢增长,只有在我完成循环后,它才达到8GiB。然而,如果我将所有值初始化为零,gnome系统监视器会显示高达8GiB的快速增长,然后开始循环。
第2版:我使用的是Ubuntu 22.04.1 LTS
它与优化器几乎没有关系。这里没有发生什么壮观的事。你的程序不会跳过任何行,它完全按照你的要求执行。
问题是,当您分配内存时,您同时与分配器和操作系统的分页系统进行接口。最有可能的是,你的操作系统并没有让所有这些页面都驻留在内存中,而是让一些页面标记为由你的程序分配的,并且只有当你真正使用它时,才会让这些内存真正存在。这就是大多数操作系统的工作方式。
要解决这个问题,您需要与系统的虚拟内存分配器接口,使页面常驻。在Linux上,也有可能对您有所帮助的hugepage。在Windows上,有VirtualAlloc api,但我还没有深入研究这个平台。
你似乎误解了情况。用户空间进程中的虚拟内存(在本例中为堆空间(确实会"立即"分配(可能是在几个协商更大堆的系统调用之后(。
然而,您尚未接触的每个页面对齐的页面大小的虚拟内存块最初都会缺少物理页面支持。虚拟页面被延迟映射到物理页面,(仅(在需要时。
也就是说,您观察到的"分配"(作为对大堆空间的第一次访问的一部分(发生在GCC可以直接影响并由操作系统的分页机制处理的抽象层以下。
附带说明:例如,另一个后果是,在一台RAM为128 GB的机器上分配1 TB的虚拟内存块,只要你永远不会访问大部分巨大(懒惰(分配的空间,就会显得非常好。(如果需要,有一些配置选项可以限制内存过度使用。(
当您第一次接触新分配的虚拟内存页面时,每个页面都会导致页面故障,因此您的CPU最终会进入内核中的处理程序。内核评估情况并确定访问实际上是合法的。因此,它"物化"了虚拟内存页面,即选择一个物理页面来支持虚拟页面,并更新其记账数据结构和(同样重要的是(硬件页面映射机制(例如,页面表或TLB,取决于体系结构(。然后内核切换回用户空间进程,这将不知道所有这些都刚刚发生。对每页重复此操作。
据推测,上面的描述过于简单化了。(例如,可以有多个页面大小,以在映射维护效率和粒度/碎片等之间取得平衡。(
确保内存缓冲区得到硬件支持的一个简单而丑陋的方法是在您的体系结构上找到尽可能小的页面大小(例如,在x86_64上为4 kiB,因此是这些整数中的1024个(在大多数情况下((,然后预先触摸该内存的每个(可能的(页面,如:for (size_t i = 0; i < 0x80000000; i += 1024) buffer[i] = 1;
。
当然,还有比这更合理的解决方案↑;这只是一个例子来说明发生了什么以及为什么。