【问题标题】:"live C++ objects that live in memory mapped files"?“活在内存映射文件中的 C++ 对象”?
【发布时间】:2011-11-01 19:44:50
【问题描述】:

因此,我在 Gamasutra 阅读了 John Carmack 的采访,其中他谈到了他所谓的“存在于内存映射文件中的实时 C++ 对象”。以下是一些引用:

JC:是的。实际上,我从中获得了很多好处……上一个 iOS Rage 项目,我们提供了一些新技术,这些技术使用一些聪明的东西来制作活的 C++ 对象,这些对象存在于内存映射文件中,由闪存文件系统支持在这里,这就是我想在 PC 上构建我们所有未来工作的方式。

...

我对自己的命令是,我希望在我们的 PC 平台上加载两秒的游戏,这样我们就可以更快地迭代。而现在,即使使用固态驱动器,您在加载时所做的所有事情都被支配,因此需要这种不同的纪律才能说“一切都将被抽取并用于相对地址”所以你只需说,“映射文件,我所有的资源都在那里,并且在 15 毫秒内完成。”

(完整采访可以在here找到)

有没有人知道卡马克在说什么以及如何设置这样的东西?我在网上搜索了一会儿,但似乎找不到任何相关内容。

【问题讨论】:

  • 我认为他正在反序列化闪存中的 C++“不可变”对象。这总是有点复杂/有风险,因为您通常无法控制对象的内存/资源分配,除非您为对象“编写”代码。
  • 你在做移动开发吗?当您需要能够快速切换到应用程序时,这听起来很有用,这在普通计算机上并不是一个真正的问题。
  • @Kerrek 对于任何平台上的游戏或任何需要从磁盘加载大量状态的东西都很有用。
  • @Justicle:毫无疑问,它对序列化有好处,但与使用真实系统内存相比,在桌面上会不会相对较慢?
  • 这就是重点,它可以加快从磁盘加载到系统内存的速度。

标签: c++ memory-mapped-files memory-mapping


【解决方案1】:

这个想法是,您可以通过内存映射访问该文件,从而始终将您的全部或部分程序状态序列化到一个文件中。这将要求您没有通常的指针,因为指针仅在您的进程持续时才有效。相反,您必须从映射开始存储偏移量,以便在重新启动程序并重新映射文​​件时可以继续使用它。这种方案的优点是你没有单独的序列化,这意味着你没有额外的代码,你不需要一次保存所有状态——相反,你的(全部或大部分)程序状态是始终由文件支持。

【讨论】:

  • 缺点是在失败的情况下你可能会得到损坏的数据。
  • 或者,您可以使用指针,但也可以存储地图库。然后你可以在加载时重新定位你的指针。
  • 有关一些见解,请参阅 Microsoft 的 __based keyword
  • 或者你可以使用 Memento 并序列化 memento 而不是你的真实对象;)。
【解决方案2】:

您可以直接或通过自定义分配器使用placement new。

查看EASTL 以了解(子集)STL 的实现,该实现专门适用于自定义分配方案(例如在嵌入式系统或游戏控制台上运行的游戏所需)。

这里有一个免费的 EASTL 子集:

【讨论】:

    【解决方案3】:

    多年来,我们一直使用我们称之为“相对指针”的东西,它是某种智能指针。它本质上是非标准的,但在大多数平台上都很好用。它的结构如下:

    template<class T>
    class rptr
    {
        size_t offset;
    public:
        T* operator->() { return reinterpret_cast<T*>(reinterpret_cast<char*>(this)+offset); }
    };
    

    这要求所有对象都存储在同一个共享内存中(也可以是文件映射)。它通常还要求我们只在其中存储我们自己的兼容类型,以及编写自己的分配器来管理该内存。

    为了始终获得一致的数据,我们通过 COW mmap 技巧使用快照(在 linux 上的用户空间中工作,不了解其他操作系统)。

    随着向 64 位的大迁移,我们有时也只使用固定映射,因为相对指针会产生一些运行时开销。通常使用 48 位的地址空间,我们为我们的应用程序选择了一个保留的内存区域,我们总是将这样的文件映射到。

    【讨论】:

    • 使用固定地址将文件映射到其中存在危险 - 操作系统可能会在以后更改规则并将该地址范围用于其他目的。例如,它可以将程序代码加载到您为数据保留的地址范围内。
    • 确实,这就是为什么我们使用操作系统没有计划用于任何用途的保留区域。
    • @PlasmaHH 你如何找到这样的保留区域?他们在哪里记录?它们在每个操作系统上是否不同?
    • @chrisdew:它们在每个操作系统和操作系统配置上都不同。 Linux 能够通过/proc 检查您的内容映射到哪里,因此您可以找到操作系统似乎从未放置任何内容的感兴趣区域。如果您想更加确定,请阅读操作系统的源代码。
    • reinterpret_cast&lt;char*&gt;(this) 是如何工作的?为什么不使用 global_mmap_offset 变量?
    【解决方案4】:

    这让我想起了一个文件系统,我想出了一个在极短的时间内加载 CD 级别文件的文件系统(它将加载时间从 10 秒缩短到接近瞬时),并且它也适用于非 CD 媒体。它由三个版本的类组成,用于包装文件 IO 函数,都具有相同的接口:

    class IFile
    {
    public:
      IFile (class FileSystem &owner);
      virtual Seek (...);
      virtual Read (...);
      virtual GetFilePosition ();
    };
    

    还有一个额外的课程:

    class FileSystem
    {
    public:
      BeginStreaming (filename);
      EndStreaming ();
      IFile *CreateFile ();
    };
    

    你会像这样编写加载代码:

    void LoadLevel (levelname)
    {
      FileSystem fs;
      fs.BeginStreaming (levelname);
      IFile *file = fs.CreateFile (level_map_name);
      ReadLevelMap (fs, file);
      delete file;
      fs.EndStreaming ();
    }
    
    void ReadLevelMap (FileSystem &fs, IFile *file)
    {
      read some data from fs
      get names of other files to load (like textures, object definitions, etc...)
      for each texture file
      {
        IFile *texture_file = fs.CreateFile (some other file name)
        CreateTexture (texture_file);
        delete texture_file;
      }
    }
    

    然后,您将拥有三种操作模式:调试模式、流文件构建模式和发布模式。

    在每种模式下,FileSystem 对象都会创建不同的 IFile 对象。

    在调试模式下,IFile 对象只是包装了标准的 IO 函数。

    在流文件构建中,IFile 对象还包装了标准 IO,但具有将读取的每个字节写入流文件(所有者 FileSystem 打开流文件)以及写入任何文件的返回值的附加功能指针位置查询(因此,如果需要知道文件大小,则将该信息写入流文件)。这会将各种文件连接成一个大文件,但只有实际读取的数据。

    发布模式将创建一个不打开文件或在文件中查找的 IFile,它只是从流文件中读取(由所有者 FileSystem 对象打开)。

    这意味着在发布模式下,所有数据都是在一系列连续读取中读取的(操作系统会很好地缓冲它),而不是大量的搜索和读取。这对于寻道时间非常慢的 CD 来说是理想的选择。不用说,这是为基于 CD 的控制台系统开发的。

    一个副作用是数据被剥离了通常会被跳过的不必要的元数据。

    它确实有缺点 - 一个级别的所有数据都在一个文件中。这些可能会变得非常大,并且数据无法在文件之间共享,如果您有一组纹理,例如,它们在两个或多个级别中是通用的,则数据将在每个流文件中复制。另外,每次加载数据的加载过程都必须相同,不能有条件地跳过或添加元素到关卡。

    【讨论】:

    • @Justicle:我不同意:“我希望在我们的 PC 平台上加载两秒钟的游戏”,这是一种非常快速加载游戏数据的方式。
    • 问题是关于使用偏移指针序列化内存映射文件中的对象,您的示例是关于连接文件(一种不同的技术)。引用不是问题,而是参考。
    • 即您的代码很酷,但它没有回答所提出的问题。
    【解决方案5】:

    正如 Carmack 所指出的,许多游戏(和其他应用程序)加载代码是结构化的,就像许多小的读取和分配一样。

    您无需执行此操作,而是执行单个 fread(或等效项),例如将关卡文件放入内存,然后再修复指针。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2016-12-08
      • 1970-01-01
      • 1970-01-01
      • 2019-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多