【问题标题】:c++ Reading the first few bytes out of every X bytes efficientlyc ++有效地从每个X字节中读取前几个字节
【发布时间】:2016-08-05 12:57:44
【问题描述】:

我想从文件的每个 X*16 字节中读取前 16 个字节。我写的代码可以工作,但是很慢,因为有很多函数调用。

std::vector<Vertex> readFile(int maxVertexCount) {
    std::ifstream in = std::ifstream(fileName, std::ios::binary);
    in.seekg(0, in.end);
    int fileLength = in.tellg();
    int vertexCount = fileLength / 16;

    int stepSize = std::max(1, vertexCount / maxVertexCount);

    std::vector<Vertex> vertices;
    vertices.reserve(vertexCount / stepSize);
    for (int i = 0; i < vertexCount; i += stepSize) {
        in.seekg(i * 16, in.beg);
        char bytes[16];
        in.read(bytes, 16);
        vertices.push_back(Vertex(bytes));
    }
    in.close();
}

有人可以给我一些建议来提高这段代码的性能吗?

【问题讨论】:

    标签: c++ windows performance file bytebuffer


    【解决方案1】:

    不要使用seek,我会mmap 整个文件,然后简单地读取所需位置的字节。

    我不会为你写代码,但它应该是这样的:

    1. 打开文件,计算文件大小
    2. mmap整个文件
    3. 在步长中迭代计算每个块的地址
    4. 根据块构造每个Vertex并推入向量中
    5. 返回向量。

    【讨论】:

    • 感谢您的回复。这似乎是我的更好的解决方案。
    • mmap 不是一个选项,因为 Windows。我尝试使用 ifstream 来读取整个文件,但是第 2 步会变慢(整个文件可以达到 TB)。
    • 好吧,你并没有真正指定约束......无论如何,Windows IIRC 有CreateFileMapping?对于一个大文件,你需要做的就是映射顶点大小的块,相当大的东西,所以你不会做太多的映射(取决于你有多少内存。)上面的算法不会改变说了这么多,把第2步改成映射块,再给第2步加个循环!
    • mmap/CreateFileMapping 如果您的 stepSize 大到足以跳过整个扇区/块(例如,使用 4K 块大小,stepSize > 256)会有所帮助。当然,在 SSD 上,旋转磁盘的里程数可能会有所不同。不幸的是,分散读取的核心问题可能仍然存在。
    • 我猜 CreateFile 作为 Ifstream 效果更好。当我按照 peterchen 的建议实现快照/mipmap 级别时。
    【解决方案2】:

    可能不是函数调用自身,而是非顺序访问模式,从大文件中挑选小段。即使您只读取 16 个字节,存储子系统也可能会读取(并缓存)更大的块。您的访问模式对于典型的 I/O 来说是致命的。

    (分析应该显示磁盘访问是否是瓶颈。如果是“许多函数调用”,CPU 会。)

    那么,首先,你能改变这个要求吗?
    这在所有情况下都是最简单的出路。

    你能少撒点吗?例如。不是读取顶点 0, 20, 40, ..., 1000 ,而是读取顶点 0,1,2,3,4, 100, 101, 102, 103, 104, 200, 201, 202, 203, 204, .. . - 相同数量的顶点,来自文件的“所有部分”。

    第二,针对操作系统的优化
    没有可移植的方法来控制操作系统级别的缓存。

    按照@Nim 的建议,一种解决方案是内存映射文件(Windows 上为CreaterFileMapping,Linuxy 系统上为mmap)。这可以省略一个内存副本,但仍将读取整个文件。

    对 Linux 帮助不大,但在 Windows 上,您可以将 CreateFile 作为参数:

    • FILE_FLAG_NO_BUFFERING 这基本上意味着 进行缓冲,让您更好地控制发生的缓存,但您不能随意寻找 + 阅读。

    • FILE_FLAG_SEQUENTIAL_SCAN 只是告诉缓存不要存储旧数据

    这些都不能解决您的访问模式的问题,但第一个可能会在一定程度上调解它 - 特别是如果您的步骤大于磁盘扇区,而第二个可能会承受来自子系统的压力。

    第三,快照。
    最好的选择可能是将交错的快照存储在关联文件中。

    快照可能只是您操作的结果,对于特定的maxVertexCount。或多个快照,例如 mipmapping。这个想法是用顺序读取代替分散读取。

    或者,快照可以以交错顺序存储数据。对于 128 个顶点,您可以按该顺序存储顶点(粗略地说,要注意 off-by

    64, 32, 96, 16, 48, 80, 112 8, 24, 40, 56, 72, 88, 104, 120 ...

    无论您是读取前 3 个还是前 7 个或前 15 个或前 31... 值,样本都在文件中均匀分布,就像在您的原始代码中一样。在内存中重新排列它们会更快 - 特别是如果它只是一个小子集。

    注意:您需要一个强大的算法来检测您的快照是否过期,这与在不同文件系统上的“最后写入日期”发生的许多有趣的事情无关。主文件中的“更改计数器”是最安全的(尽管它会增加更改成本)。

    四、更改文件格式

    (如果您可以控制它) 上面建议的交错存储可用于整个文件。但是,这对处理有很大的影响——尤其是当您需要在某个时候恢复“原始”订单时。

    一个优雅的选择是将这样一个交错子集作为文件的一部分,原始顺序的完整顶点列表。有一个截止点stepSize 不再有太大帮助,可能大约是磁盘的 2*sector/block 大小。所以文件大小只会增加几个百分点。但是,写入的成本会更高一些,顶点数量的变化会明显更糟。


    别名警告

    如果这是为了获得“统计”或“视觉上足够”的采样,则固定的stepSize 可能会出现问题,因为它可以对数据中存在的任何模式产生混叠效果(想想莫尔模式)。

    在这种情况下,随机抽样是可取的。这可能听起来很可怕,并使上面的一些解决方案更加困难,但通常是避免许多次优情况的最简单方法。

    【讨论】:

    • 我喜欢你对快照/mipmap 级别的想法。我会尝试一下。对于混叠警告:目前数据在使用固定步长进行二次采样后看起来还不错,但我会在未来研究它。
    【解决方案3】:

    ...如果由于某种原因您不能使用map,请将文件读入“巨大的吞咽”中的缓冲区...缓冲区大小是X 字节的倍数。继续读取该缓冲区(注意注意读取了多少字节)。直到你到达文件的末尾。

    您特别要避免的是一大堆物理 I/O 操作:磁盘读/写机制的移动。出于这个原因,操作系统喜欢缓冲事​​物,但它只能猜测 您的 应用程序正在尝试做什么,并且可能会猜错。一旦磁盘将读/写磁头定位到正确的磁道(“寻道时间”),它就可以在一次旋转中检索整个磁道的数据。但是“seek time”比较慢。

    映射文件,然后非随机地读取映射文件中的数据,显然是最有利的策略,因为现在操作系统知道到底发生了什么.

    【讨论】:

      【解决方案4】:

      首先,我认为您正在从定义中返回向量按值,即使您发布的代码缺少返回语句,因此必须复制向量。通过引用将其传递到您的方法中,因此不需要复制。

      而且你可以使用低级的pread()来读取而不需要寻找:

      void readFile( size_t maxVertexCount, std::vector<Vertex> &vertices )
      {
          struct stat sb;
          int fd = std::open( fileName, O_RDONLY );
          std::fstat( fd, &sb );
      
          size_t vertexCount = sb.st_size / 16;
      
          size_t stepSize = std::max( 1, vertexCount / maxVertexCount );
      
          vertices.reserve( vertexCount / stepSize );
      
          for ( off_t i = 0; i < vertexCount; i += stepSize)
          {
              char bytes[ 16 ];
              std::pread( fd, bytes, 16, 16 * i );
              vertices.push_back( Vertex( bytes ) );
          }
      
          std::close( fd );
      }
      

      您应该能够找出所需的错误处理和头文件。

      这利用了内核的页面缓存和可能的预读。根据您的操作系统和配置,mmap() 或读取整个文件等其他方法可能更快,也可能不会更快。

      【讨论】:

      • 谢谢,我试试看。祈祷 pread() 不会读取和缓存更大的块,就像 peterchen 解释的那样。
      猜你喜欢
      • 1970-01-01
      • 2017-06-24
      • 1970-01-01
      • 1970-01-01
      • 2020-11-19
      • 2019-03-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多