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
整个文件,然后简单地读取所需位置的字节。
我不会为您编写代码,但它应该遵循以下原则:
- 打开文件,计算文件大小
mmap
整个文件
- 迭代步长计算每个块的地址
- 根据块构建每个
Vertex
并推入向量
- 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()
或读取整个文件可能会或可能不会更快。
我想从文件的每 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
整个文件,然后简单地读取所需位置的字节。
我不会为您编写代码,但它应该遵循以下原则:
- 打开文件,计算文件大小
mmap
整个文件- 迭代步长计算每个块的地址
- 根据块构建每个
Vertex
并推入向量 - 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()
或读取整个文件可能会或可能不会更快。