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

时间:2015-11-05 09:10:37

标签: c++ pointers memory-management cross-platform 32bit-64bit

我希望就如何处理我即将进行的设计提出一些高级建议。

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

进一步背景

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

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

我考虑过

  1. 将有效负载存储在数组中,使得索引暗示(并暗示)“树地址”,并且可以使用该索引上的简单算术计算给定索引的邻居。不幸的是,这需要我根据树的最大深度调整数组的大小,这是我事先不知道的,并且由于较低级别中的空节点元素,它可能会产生更大的内存开销,因为不是树的所有分支走到同样的深度。

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

  3. 假设所有这些指针中最重要的32位为零,如果不是则抛出异常,并且仅存储最低有效32位。因此可以根据需要重建所需的指针。该系统可能使用超过4GB,但该过程永远不会。我只是假设指针偏离了进程基地址,并且不知道这对于公共平台(Windows,Linux,OSX)有多安全(如果有的话)。

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

  5. 有什么建议吗?关于最后一个想法(我目前认为这是我最好的候选人),我可以假设在一个使用少于2GB的进程中,动态分配的对象将彼此相差2 GB以内吗?或者根本不是吗?

4 个答案:

答案 0 :(得分:4)

将问题2和4结合起来,将所有节点放入一个大数组,并存储例如ei - fh = 0 di - fg = 0 dh - eg = 0 。然后你可以从int32_t neighborOffset = neighborIndex - thisIndex获得邻居。这摆脱了2和4的缺点/假设。

答案 1 :(得分:3)

如果在Linux上,您可以考虑使用(并编译)x32 ABI。恕我直言,这是您的问题的首选解决方案。

或者,不要使用指针,而是将索引编入大型数组(或C ++中的std::vector),这可能是全局变量或static变量。管理单个巨大的堆分配节点数组,并使用节点索引而不是指向节点的指针。就像你的§2一样,但由于数组是全局或static数据,你不需要在任何地方传递它。

(我想优化编译器能够生成聪明的代码,这几乎和使用指针一样有效)

答案 2 :(得分:0)

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

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

简短回答:不要对任何C ++指针的大小做出任何假设。

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

答案 3 :(得分:0)

通过利用内存区域的对齐来“自动”找到数组的基址,可以消除(2)的缺点。例如,如果要支持最多4 GB的节点,请确保节点阵列以4 GB的边界开始。

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

这是被接受的解决方案的“绝对”变体,即“相对”。此解决方案的一个优点是index在树中始终具有相同的含义,而在相对解决方案中,您确实需要(node_address, index)对来解释索引(当然,您可以在有用的相对场景中使用绝对索引。这意味着当您复制节点时,您不需要调整它包含的任何索引值。

“相对”解决方案在其索引中相对于此解决方案也丢失了1个有效索引位,因为它需要存储带符号的偏移量,因此使用32位索引,您只能支持2 ^ 31个节点(假设完全压缩)尾随零位,否则它只是节点的2 ^ 31 字节

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

最后,您还可以在树本身内利用这种对齐思想来“隐式”存储其他指针。例如,父节点可能存储在N字节对齐的边界,然后所有子节点都存储在相同的N字节块中,因此它们“隐式地”知道它们的父节点。这有多可行取决于你的树的动态程度,扇出的变化程度等等。

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

这种技巧实际上只适用于64位程序,可用的虚拟地址空间非常大,因此64位程序也可以使用,并且还可以取消。