c ++有效地从每个X字节中读取前几个字节

c++ Reading the first few bytes out of every X bytes efficiently

我想从文件的每 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();
}

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

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

我不会为您编写代码,但它应该遵循以下原则:

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

... 如果由于某种原因您不能使用 map,请将文件读入 "great big gulps" 中的缓冲区 ... 缓冲区大小是 [= 的倍数11=] 字节。继续读入该缓冲区(注意 读取了多少字节。直到到达文件末尾。

您特别要避免的是一大堆物理 I/O操作:磁盘的移动read/write 机制。由于这个原因,操作系统喜欢缓冲东西,但它只能 猜测 你的 应用程序正在尝试做什么,它可能猜错了。一旦磁盘将 read/write 磁头定位到正确的磁道 ("seek time"),它就可以在一次旋转中检索整个磁道的数据价值。但是"seek time"比较慢。

映射文件,然后非随机读取映射文件中的数据,显然是最有利的策略,因为现在操作系统知道到底是怎么回事。

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

(分析应该显示磁盘访问是否是瓶颈。如果是 "many function calls",CPU 就是。)

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

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

二、OS具体优化.
没有可移植的方法来控制 OS 级缓存。

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

对 Linux 帮助不大,但在 Windows 上,您有 CreateFile 的参数:

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

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

这些都不能解决您的访问模式的问题,但第一个可能会有所调解 - 特别是如果您的步骤大于磁盘扇区,第二个可能需要来自子系统的压力。

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

对于特定的 maxVertexCount,快照可能只是您操作的结果。或多个快照,如 mipmapping。思路是用顺序读代替散读

或者,快照可以交错顺序存储数据。对于 128 个顶点,您可以按该顺序存储顶点(粗略地提防 off-by<-one、零对一和混叠效果,以及我的错误):

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

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

注意:您需要一个强大的算法来检测您的快照是否已过时,独立于 "last write date" 在不同文件系统上发生的许多有趣的事情。主文件中的 "change counter" 将是最安全的(尽管它会增加更改成本)。

四、更改文件格式

(如果你能控制的话) 上面建议的交错存储可以用于整个文件。但是,这对处理有很大的影响——尤其是当您需要在某个时候恢复 "original" 顺序时。

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


混叠警告

如果这是为了获得 "statistical" 或 "visually sufficient" 采样,固定的 stepSize 可能会有问题,因为它可以在任何模式下产生混叠效果(想想波纹模式)存在于数据中。

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

首先,我认为即使您发布的代码缺少 return 语句,您也是根据定义 按值 计算向量 ,所以向量必须被复制。通过引用将其传递到您的方法中,因此无需复制。

并且可以使用低级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 );
}

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

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