【问题标题】:How to avoid wasting memory on 64-bit pointers如何避免在 64 位指针上浪费内存
【发布时间】:2016-02-06 01:03:00
【问题描述】:

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

解决我的问题的直接方法将导致数以百万计的指针。在 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 的范围内(并抛出 if它不是)。然后任何节点都可以通过将该偏移量添加到this 来找到它的邻居。

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

【问题讨论】:

  • 速度有问题吗?如果没有,您可以编写自己的内存分配函数来处理 32 位指针。为此,您可以在内存中保留一个存储数据的区域,存储该空间的地址并使用 32 位(甚至 16 位)地址在该空间内工作。如果你能保证这个进程永远不会超过 2GB,你可以保留这么多的数量......而且每个进程都有自己的地址空间,从 0x00 开始。所以即使系统会占用更多的内存,你的进程也总是从 0x00 地址开始。
  • @Nidhoegger 这个想法似乎遇到了与我的问题中的第二个类似的问题。要让节点取消引用这些指针,它需要基地址,或指向执行取消引用的某个对象的 64 位指针。对吗?
  • 您需要编写自己的 deref 函数。给它一个 32 位地址,你的函数会添加基地址到它
  • 我可以假设在一个使用少于 2GB 的进程中,动态分配的对象将在 2GB 范围内吗? 通常不会,不会。在 Linux 上,如果您想要该保证,则需要 mmap(..., MAP_32BIT, ...)。在实践中,它恰好适用于 Linux+gcc 上不断增长的std::vector 的中等大小分配(~18kiB),但我不想假设关于malloc 的任何事情,特别是对于大型分配(如 2MiB 或更多 = 多个大页面)。见this super-hacky microbenchmark with 32-bit pointers zero-extending to 64-bit

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


【解决方案1】:

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

【讨论】:

  • 另外,这可以在 x86-64 上高效编译:编译器可以使用 movsxd 加载偏移量以符号扩展至 64 位,然后为 *(this+neighborOffset) 使用索引寻址模式可以使用索引寻址模式(具有比例,因为您的偏移量在int32_t 4 字节元素中,而不是在字节中)。但是您还需要在寄存器中计算新的当前地址。
  • 不过,在这个数据结构中进行指针追踪就像lea rsi, [rsi + rdx*4] / movsxd rdx, dword [rsi] 一样简单。 (可以在 Intel SnB 系列 CPU 上的关键路径上使用 LEA,其中[rsi] 的加载使用延迟比索引寻址模式少 1c。否则您(或编译器)可以使用额外的寄存器到 movsxd使用与 LEA 并行的索引寻址模式来获得新的current)。
  • 存储简单的索引(不是偏移量)可以有效地编译更多,假设递归变成一个循环。只需加载一个mov(无符号索引的零扩展)或movsx(有符号索引的符号扩展),基指针保持不变。但是,如果您必须传递数组指针,则会使源更丑陋。但是您确实需要将它们全部放在同一个数组中,以确保它们在 2GB 以内,除非您使用 mmap(MAP_32BIT) 分配。
【解决方案2】:

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

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

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

【讨论】:

  • 这更像是一个简短的评论而不是一个答案 ;-)
  • 不,它给出了问题的解决方案
  • 实际上,说真的,我喜欢你的数组索引想法。投赞成票。
  • 所以基本上是我的问题中的选项 2,还是放弃跨平台?
【解决方案3】:

您可以通过利用内存区域的对齐来“自动”找到数组的基地址来消除 (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 分配适当对齐的块(通常只保留大量的虚拟地址空间,然后根据需要分配它的块) - 通过 @ 以太987654327@ 参数或仅通过保留足够大的区域来保证您在该区域的某处获得所需的对齐。与公认的解决方案相比,需要弄乱分配器是主要缺点,但如果这是您程序中的主要数据结构,那么它可能是值得的。当您控制分配器时,您还有其他优势:如果您知道所有节点都分配在 2^N 字节边界上,您可以进一步“压缩”索引,因为您知道低 N 位将始终为零,所以使用 32 位索引,如果您知道它们是 32 字节对齐的,您实际上可以存储 2^(32+5) = 2^37 个节点。

这些技巧实际上只在 64 位程序中可行,因为有大量可用的虚拟地址空间,所以从某种意义上说,64 位既给又带走。

【讨论】:

    【解决方案4】:

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

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

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

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

    【讨论】:

    • 这更像是一个评论而不是一个答案。
    • 一个相当大的评论......,我确实提出了建议!
    • 不对任何 C++ 指针的大小做任何假设 的实际用途是什么?完全忽略它?您可以查看sizeof 和外来平台之外,它会如您所料。如果不是,那么您将采用另一种方法,但这是一个具有实际解决方案的实际问题。
    猜你喜欢
    • 2022-08-05
    • 2012-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-20
    • 1970-01-01
    • 1970-01-01
    • 2021-09-17
    相关资源
    最近更新 更多