Windows 上的别名物理内存

Aliasing physical memory on Windows

我有两个线程和一个大数据集。线程 R 不断地从数据集中读取数据并向用户呈现数据视图。线程 W 不断接收远程数据,对其执行一些工作并将其发布到数据集。

线程 R 需要控制它接收数据集一致视图的粒度。一种解决方案是双缓冲; W 写入一个副本,而 R 从另一个副本读取,当 R 准备好更新时,要么 W 的副本被原子地复制到 R(禁止,因为数据集很大且几乎没有变化),要么他们原子地交换副本,W 带来 R 的旧副本通过重新应用自上次交换以来的增量更改来复制最新版本(跟踪这些很烦人,而且所有增量都必须处理两次很烦人)。

我想做的是:

这避免了额外的内存复制、跟踪和重新应用增量等的需要。

然而,AFAICT 虽然 Windows 确实允许创建共享内存区域(甚至是自动写时复制内存区域),但它似乎特意使它无法显式映射W 可以使用任何方式向 R 发布新视图的物理页面。

有什么我遗漏的吗? - 是否有可能实现这样的东西,一个发布步骤完全通过改变页面映射来实现,而不需要内存复制?

即使有人会创建您想要的 API,也可以做那些 CoW 的事情。 即使您将切换到单独的进程而不是线程(每个进程只有 1 页 table,您不能让 2 个线程在同一地址上看到不同的数据)。

如果你是 modifying/remapping 随机的 4kb 页面,你会 waste gigabytes of RAM for your page table。不仅浪费内存,还会降低性能。

我觉得你看到的问题太笼统了,你试图在太低的抽象层次上解决它。

concurrency/performance 要求有多重?你能不能只锁定你的数据库,这样读取和写入就不会同时发生?

你的观点到底是什么?您能否在启动时创建视图,当新消息到达时,在线程 W 上更新数据库,并在线程 R 上更新视图?

整个事情是持久的吗?如果是,只需使用具有事务隔离功能的嵌入式 NoSQL 引擎。例如 ESENT, or if your software is cross-platform, LMDB 可能适合。

我认为应该可以通过一些技巧来做一些接近您要求的事情。

我将首先描述我认为最简单、最有效但最不灵活的方法,并将其称为方法A。要使用这种方法,数据必须按块排列,并且每个块必须完全包含在一个页面中:

  • 使用相同的文件映射对象为 W 创建一个 read/write 视图,为 R 创建一个写时复制视图。

  • 每当W要修改数据块时,它首先对写时复制视图中的相应块执行虚拟写入。

    注意:我相信写入页面会导致写时复制,即使写入实际上并未更改内容,但为了安全起见,我建议避免这种假设,这您可以通过在每个数据块中包含一个虚拟字节来做到这一点,即 R 将忽略的字节。然后 W 可以增加虚拟字节以确保复制相应的页面。

  • 要同步,丢弃现有的写时复制视图并创建一个新视图。

我希望不必要的虚拟写入的开销可以忽略不计,但对齐块以使它们不与页面边界重叠可能会很不方便。

如果是这样,方法 B 与方法 A 相同,除了有多个虚拟字节,以一定间隔放置,确保与块重叠的每一页至少包含一个虚拟字节.这会增加虚拟写入的开销,但我不希望它过多。

但是,W 每次需要进行更改时显式地进行这些虚拟写入可能会很尴尬,例如,如果数据实际上没有按块排列,或者每个块内有多个虚拟字节会很不方便。因此,我们应该考虑方法C:

  • 为W创建只读和读写视图,为R创建写时复制视图。让W使用只读视图读取数据但是写入它的读写视图。

  • 使用VirtualProtect和PAGE_GUARD保护读写视图中的所有页面。

  • 当触发保护页面错误时,让异常处理程序对写时复制视图中的相应页面进行虚拟写入。矢量异常处理程序在我看来是最干净的选择。

    注意:我的研究表明,尽管不是很明确,这将起作用,尽管它涉及在页面错误处理程序中故意调用页面错误。它应该得到支持,因为任何异常处理程序都没有合理的方法来确保它不引用被调出的数据,但由于我还没有找到明确的声明,建议进行一些实验。

方法 C 的效率可能低于 A 或 B,因为它需要处理额外的页面错误异常,并相应地额外往返于内核模式并返回。我也不确定跟踪保护页面所涉及的页面 table 开销。但是,它可能更方便,因为从处理代码中消除虚拟写入减少了代码需要了解缓冲的程度。

最终变体避免了处理代码需要知道缓冲完全通过使用单个视图,无论 W 是读还是写。 方法D如下:

  • 为 W 创建一个 read/write 视图,为 R 创建一个写时复制视图。

  • 使用 VirtualProtect 将 read/write 视图的所有页面的权限更改为只读。

  • 当页面错误被触发时,让异常处理程序将错误页面的权限更改为 read/write 并在写时复制中对相应页面进行虚拟写入查看.

我认为这种方法效率最低,因为我预计显式更改块的权限会比使用保护页慢得多。它还可能导致页面出现更多碎片 table。但是,如果事实证明它的性能足够好,那几乎肯定是最方便的解决方案。


一些补充说明:

我相信所有这些方法都应该有效,但要注意在处理第一个页面错误时触发第二个页面错误时究竟发生了什么。我对不同变体的比较效率没有信心。做一些比较测试可能是明智的。

文件映射对象可能应该由页面文件支持,您可能想尝试使用大页面。这会增加需要复制的数据量,但会减少页面上的负载 table。同样,比较测试可能是合适的。

我假设您已经考虑过这一点,但未来的读者应该注意,根据数据的性质,使用映射可能根本不明智。例如:

  • 数据块可以有两个修订号,一个表示块何时生效,另一个表示应该被删除。这种方法的时间开销很小,R 只需要在处理块时检查修订号,以便它可以跳过太新的块并删除过时的块。这涉及较少的数据复制:W 只需要复制它正在处理的数据块,而不是整个页面,并且 adding/deleting 块根本不需要复制任何数据。

  • 如果块需要按特定顺序链接,修订号可能不够,但您可以为 R 和 W 设置单独的链。同步需要您重新链接块,但是这仍然可能比修改页面 table.

  • 更快