“生活在内存映射文件中的实时C ++对象”?

时间:2011-08-23 10:00:52

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

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

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

...

  

我在这里向自己发出命令,我希望在我们的PC平台上玩两秒钟的游戏,所以我们可以更快地迭代。而就在现在,即使使用固态驱动器,你也会受到加载时所做的所有事情的支配,因此需要这种不同的规则才能说“一切都将被毁灭并用于相对地址”。所以你只要说,“映射文件,我所有的资源都在那里,它在15毫秒内完成。”

(可以找到完整的访谈here

有人知道Carmack在谈论什么以及你将如何设置这样的东西?我在网上搜索了一下,但我似乎找不到任何东西。

5 个答案:

答案 0 :(得分:7)

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

答案 1 :(得分:1)

您可以直接使用placement new,也可以使用自定义分配器。

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

EASTL的免费子集在这里:

答案 2 :(得分:1)

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

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位的地址空间,我们为应用程序选择了一个保留的memry区域,我们总是将这样的文件映射到。

答案 3 :(得分:1)

这让我想起了一个文件系统,我提出了在很短的时间内加载了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的控制台系统开发的。

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

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

答案 4 :(得分:0)

由于Carmack表示许多游戏(和其他应用程序)加载代码的结构很简单,所以很多小的读取和分配。

而不是这样做,你只需要一个fread(或等效的)将一个级别文件写入内存,然后再修改指针。