【问题标题】:Using MMU to implement resizable arrays使用 MMU 实现可调整大小的数组
【发布时间】:2017-05-20 18:51:54
【问题描述】:

通常,列表要么实现为链表,遍历速度很慢,要么实现为数组列表,在插入元素时很慢。

我想知道是否可以通过重新映射而不是在插入或删除元素时复制内存来更有效地使用处理器的 MMU 来实现列表。这意味着数组中任何位置的索引和插入/删除都具有 O(1) 的速度,better than any other list implementation

我的问题是:

  • 程序是否真的能够控制自己的虚拟内存,或者是否需要对操作系统进行更改?
  • 每个进程的页表条目数是否有限制?条目越多,内存访问是否越慢?
  • 更改页表条目是否如此缓慢,以至于仅对非常大的列表才更有效?
  • 是否有此类列表的任何现有实现?如果是,是什么阻止人们更多地使用它们?

【问题讨论】:

  • 将此链接视为一个很好的入口点:msdn.microsoft.com/en-us/library/windows/desktop/… 底线是虚拟内存管理可以由应用程序控制。但是处理的页面不是任意大小的。因此,如果您想尝试一下自己的想法,请继续尝试。但是,如果您的列表项小于页面大小,您将浪费大量(虚拟)内存......无论如何,我怀疑您最终会得到一些有用/执行的东西。但值得称赞的是,具有创造性精神!
  • 大多数现代 CPU 使用中等大小但相当大的页面大小。 4kb 和 8kb 是典型的。 MMU 只能用于映射页面大小的偶数倍的内存块。除非您的对象具有精确的对齐方式,并且它们的大小是页面大小的偶数倍,否则 MMU 是无用的。即使你把那只鸭子排成一排:祝你编写自己的操作系统好运!
  • 如果您对最新和最先进的数据结构感兴趣:查找“持久数据结构”。想法来自函数式编程,但也可以在 C++/Rust 或其他系统代码中使用。
  • 你不应该把 O() 表示法看得太认真。对于任何合理的 $n$,$O(1)$ 和 $O(\log n)$ 之间的差值可以小于乘法常数的差值。使用MMU意味着调用OS,成本很高。

标签: c++ list hardware-acceleration mmu


【解决方案1】:

首先对您的问题进行一些具体的回答:

  • 是的,在许多操作系统上,程序对其虚拟内存有很大的控制权,例如,mmap 在类 UNIX 操作系统上,similar APIs 在 Windows 上。特别是 Linux 最近添加了 several methods 以允许从内核对用户可见的缓冲区进行高级操作而无需复制 - 但其中一个有趣的是 no longer for this world(至少在性能方面)。
  • 通常,每个进程的页表条目数没有具体限制。当然,您可能会遇到其他限制,例如每个进程的内存限制、物理内存限制等。对内存的访问通常不会随着条目的增多而变慢。当然,访问更多页面可能意味着访问速度变慢(例如,因为您超过了 TLB 的大小) - 但这并不是更多页面的直接功能。页表条目本身只是位于 RAM 中,因此您可以毫无问题地拥有数百万个条目。
  • 在现代操作系统上更改页表条目的速度合理。例如,在我的笔记本电脑上,更改页表条目似乎每页大约需要 120 ns(加上系统调用的一些固定开销)。
  • 是的,您可以在那里找到examples,但它们通常针对相当狭窄的场景。事实上,您可以看到 mach 的 libc 尝试使用 use MMU tricks 来作为一个重要的例程 than memcpy

讨论

使用 MMU 技巧的核心问题是 (a) 你只能“零复制”整个页面,这几乎意味着 4K 或更大的粒度,以及类似的限制性对齐 (b) 即使mmap-类型调用很快,高效的内存复制例程也是如此!

让我们首先看一下(a)。如果我对您的理解正确,您希望通过使用 MMU 技巧来移动插入发生时需要移动的元素,从而加速插入 std::vector 之类的东西。问题是您只能在典型系统上移动 0、4096、8192 等字节!因此,如果您将一个 4 字节的 int 插入到 vector<int> 中会有什么帮助?您也许可以在插入点将vector 的底层存储“破解”为两部分并跟踪它,希望在某个点再次合并它们(例如,如果您插入 4096 字节的东西) - 但你结束了使用不同的数据结构,具有不同的属性,无论如何,MMU 技巧在这里并不是真正的基础。

这将我们带到(b)。理所当然地认为,在我的机器上,一个页面可以在大约 120 ns 内重新映射(通过mmap)。这看起来很快(考虑到它涉及获取各种内核锁、弄乱页表、添加 VMA 等,这还不错)——但是复制内存非常快。在同一个盒子上,我可以以大约 12 GB/s 的速度在内存中复制(即从任何缓存级别的 RAM 复制),而 L1 或 L2 中的复制可能以 80-100 GB/s 的速度进行。因此,复制 4K 页面需要 41 ns(缓存)和 340 ns(未缓存,到 RAM)之间。因此,即使 可能 搞乱页表也不是一个明显的胜利,尤其是在缓存的情况下(缓存的情况可能是占主导地位的情况,在大多数工作负载上平均)。

所以这些类型的技巧可能有意义,但仅限于特定场景,例如:

  • 您有一些方法可以处理页面映射只能在页面粒度的块中移动/复制/移位的事实,例如,因为您的结构恰好是页面粒度的倍数,或者您使用批量插入是页面粒度等的倍数。
  • 您有一些方法可以更快地映射页面:例如,使用 2MB 页面而不是 4K 页面,或者编写一些内核端代码来加速您的用例。
  • 您想使用比简单移动内存更花哨的技巧,例如。让相同的数据同时出现在两个地方,实现 COW 结构或类似的东西。

重新分配

MMU 技巧最常见和最有用的例子可能是realloc。在 Linux 和 Windows (it seems?) 上,realloc 可以通过重新映射和扩展内存中的映射页面(又名 MMU 技巧)来实现,这既避免了物理复制,也无需临时拥有旧分配的区域和新区域立即“存活”(如果它们的总和接近物理内存的大小,这可能很难)。

特别是,最新版本的 Linux 可能会使用 mremaprealloc 堆区域,这些区域最初是 mmaped(默认情况下,分配请求大于 128K 时会发生这种情况,但也可能会发生这种情况当sbrk 的可用空间用完时)。

【讨论】:

  • 也许结合持久数据结构,这确实是一个好主意。不是用于管理单个数组项,而是用于管理块...en.wikipedia.org/wiki/Persistent_data_structure
  • 在多处理器机器和多线程应用程序上,使 TLB 失效可能会更慢。
  • @BitTickler - 可以肯定的是:持久结构中可能发生的“逻辑复制”操作以及如果您想做某种类型的 COW 则需要“捕获写入”都是可能 非常适合 MMU 帮助。当然,细节是赚大钱的,而且有许多障碍需要跨越(例如,在多线程环境中捕获写入并将它们有效地映射回用户结构等)。
  • @Non-maskableInterrupt - 原则上是的。请记住,mmap 通常不会使 TLB 无效,因为它只是向进程的页表添加条目,而不是删除它们(而且我不知道有任何现代 TLB存储“负”条目)。另一方面,munmapmremap 通常确实涉及某种类型的 TLB 刷新,包括应用程序的跨处理器击落。尽管如此,在我的测试中,我测量到 munmap 的成本仅为 mmap 的一半,即使隐含 TLB 未命中也是如此。
猜你喜欢
  • 2023-04-02
  • 2011-03-24
  • 2020-12-09
  • 2010-09-14
  • 2022-01-13
  • 2015-06-27
  • 2011-02-25
  • 2021-06-16
  • 1970-01-01
相关资源
最近更新 更多