如何避免在64位指针上浪费内存

How to avoid wasting memory on 64-bit pointers

本文关键字:内存 指针 何避免 64位      更新时间:2023-10-16

我希望能得到一些关于如何处理我即将进行的设计的高级建议。

对我的问题采取直接的方法会产生数以百万计的指针。在64位系统上,这些指针可能是64位指针。但就我的应用程序而言,我认为我不需要超过32位的地址空间。然而,我仍然希望系统能够利用64位处理器的算术运算(假设这是我在64位系统上运行时得到的)。

进一步的背景

我正在实现一个树状数据结构,其中每个"节点"都包含一个8字节的有效负载,但也需要指向四个相邻节点(父节点、左子节点、中间子节点、右子节点)的指针。在使用64位指针的64位系统上,仅将8字节的有效负载链接到树中就相当于32字节,这是400%的"链接开销"。

数据结构将包含数百万个这样的节点,但我的应用程序不需要太多的内存,所以所有这些64位指针看起来都很浪费。该怎么办?有没有办法在64位系统上使用32位指针?

我考虑过

  1. 将有效载荷存储在数组中,使索引隐含(并由其隐含)"树地址",并且可以通过对该索引的简单算术来计算给定索引的邻居。不幸的是,这需要我根据树的最大深度来调整数组的大小,而我事先并不知道,而且由于较低级别的空节点元素,这可能会导致更大的内存开销,因为并非树的所有分支都具有相同的深度。

  2. 将节点存储在一个足够大的数组中,以容纳所有节点,然后使用索引而不是指向链接邻居的指针。AFAIK这里的主要缺点是每个节点都需要阵列的基址来找到它的邻居。因此,他们要么需要存储它(一百万次),要么需要在每次函数调用时传递它。我不喜欢这样。

  3. 假设所有这些指针中最高有效的32位为零,如果不是,则抛出异常,只存储最低有效的32个位。因此,可以根据需要重建所需的指针。该系统可能会使用超过4GB的空间,但这个过程永远不会。我只是假设指针从进程基址偏移,不知道在通用平台(Windows、Linux、OSX)上这会有多安全(如果有的话)。

  4. 存储64位this和指向邻居的64位指针之间的,假设该差将在int32_t的范围内(如果不在,则抛出)。然后,任何节点都可以通过将该偏移添加到this来找到它的邻居。

有什么建议吗?关于最后一个想法(我目前认为这是我的最佳选择),我是否可以假设在一个使用不到2GB的过程中,动态分配的对象之间的距离将在2GB以内?或者根本不一定?

结合问题中的想法2和4,将所有节点放入一个大数组中,并存储例如int32_t neighborOffset = neighborIndex - thisIndex。然后您可以从*(this+neighborOffset)获取邻居。这消除了2和4的缺点/假设。

如果在Linux上,您可能会考虑使用x32 ABI(并为其编译)。IMHO,这是解决您问题的首选方案。

或者,不要使用指针,而是索引到一个巨大的数组(或者C++中的std::vector)中,该数组可以是全局变量或static变量。管理单个巨大的堆分配的节点数组,并使用节点的索引而不是指向节点的指针。就像你的§2一样,但由于数组是全局或static数据,你不需要到处传递它。

(我猜优化编译器能够生成聪明的代码,其效率几乎与使用指针一样高)

您可以通过利用内存区域的对齐来"自动"找到数组的基地址,从而消除(2)的缺点。例如,如果您想支持高达4GB的节点,请确保您的节点阵列从4GB的边界开始。

然后,在地址为addr的节点中,可以将另一个位于index的节点的地址确定为addr & -(1UL << 32) + index

这是公认的"相对"解决方案的一种"绝对"变体。该解决方案的一个优点是,index在树中总是具有相同的含义,而在相对解决方案中,您确实需要(node_address, index)对来解释索引(当然,您也可以在有用的相对场景中使用绝对索引)。这意味着,当您复制一个节点时,不需要调整它包含的任何索引值。

由于"相对"解决方案需要存储带符号的偏移量,因此其索引中相对于此解决方案还会丢失1个有效索引位,因此对于32位索引,您只能支持2^31个节点(假设尾部零位完全压缩,否则只有2^31字节的节点)。

您还可以将基树结构(例如,指向根的指针以及节点本身之外的任何记账)存储在4GB地址,这意味着任何节点都可以跳到相关的基结构,而无需遍历所有父指针或其他内容。

最后,您还可以在树本身中利用这种对齐思想来"隐式"存储其他指针。例如,也许父节点存储在N字节对齐的边界上,然后所有子节点都存储在同一个N字节块中,这样它们就"隐式"知道了自己的父节点。这有多可行取决于你的树有多动态,扇出的变化有多大,等等

您可以通过编写自己的分配器来完成这类任务,该分配器使用mmap来分配适当对齐的块(通常只需保留大量的虚拟地址空间,然后根据需要分配其中的块)-通过hint参数,或者只需保留一个足够大的区域,就可以保证在该区域中的某个位置获得您想要的对齐。与公认的解决方案相比,需要摆弄分配器是主要的缺点,但如果这是程序中的主要数据结构,那么这可能是值得的。当你控制分配器时,你也有其他优势:如果你知道所有节点都分配在2^N字节的边界上,你可以进一步"压缩"索引,因为你知道低N位总是零,所以对于32位索引,如果你知道它们是32字节对齐的,你实际上可以存储2^(32+5)=2^37个节点。

这些技巧实际上只有在64位程序中才可行,因为有大量的虚拟地址空间可用,所以在某种程度上,64位既可以提供,也可以带走。

您断言64位系统必须具有64位指针是不正确的。C++标准没有做出这样的断言。

事实上,不同的指针类型可以有不同的大小:sizeof(double*)可能与sizeof(int*)不同。

简言之:不要对任何C++指针的大小做出任何假设。

在我看来,你想建立自己的内存管理框架。