我有一个非常大的二进制文件,我需要根据输入文件中的 id 创建单独的文件。有 146 个输出文件,我正在使用cstdlib
andfopen
和fwrite
. FOPEN_MAX
是 20,所以我不能同时打开所有 146 个输出文件。我还想尽量减少打开和关闭输出文件的次数。
如何有效地写入输出文件?
cstdlib
由于遗留代码,我还必须使用该库。
可执行文件还必须与 UNIX 和 Windows 跨平台兼容。
我有一个非常大的二进制文件,我需要根据输入文件中的 id 创建单独的文件。有 146 个输出文件,我正在使用cstdlib
andfopen
和fwrite
. FOPEN_MAX
是 20,所以我不能同时打开所有 146 个输出文件。我还想尽量减少打开和关闭输出文件的次数。
如何有效地写入输出文件?
cstdlib
由于遗留代码,我还必须使用该库。
可执行文件还必须与 UNIX 和 Windows 跨平台兼容。
您可能会采取几种可能的方法:
保留小于 FOPEN_MAX 的已打开输出文件句柄的缓存 - 如果需要在已打开的文件上进行写入,则只需执行写入即可。否则,关闭缓存中的一个句柄并打开输出文件。如果您的数据通常聚集在一起,特定文件集的数据在输入文件中分组在一起,那么这应该与文件句柄缓存的 LRU 策略很好地配合使用。
自己处理输出缓冲,而不是让库为您做:保留自己的 146 个(或您可能需要的多个)输出缓冲区并将输出缓冲到这些缓冲区,并在特定输出时执行打开/刷新/关闭缓冲区被填满。您甚至可以将其与上述方法结合起来,以真正最小化打开/关闭操作。
只要确保您测试好在填充或几乎填充输出缓冲区时可能发生的边缘条件。
可能还值得扫描输入文件,列出每个输出 id 并对其进行排序,以便您首先写入所有 file1 条目,然后写入所有 file2 条目等。
如果您无法以某种方式增加最大 FOPEN_MAX,您可以创建一个简单的请求队列,然后根据需要关闭并重新打开文件。
您还可以跟踪每个文件的最后写入时间,并尝试保持最近写入的文件处于打开状态。
解决方案似乎很明显——打开 N 个文件,其中 N 略小于 FOPEN_MAX。然后通读输入文件,提取前N个输出文件的内容。然后关闭输出文件,倒回输入,然后重复。
首先,我希望您尽可能多地并行运行。没有理由不能同时写入多个文件。我建议按照 thomask 所说的去做并排队请求。然后,您可以使用一些线程同步来等待整个队列被刷新,然后再允许下一轮写入通过。
您没有提到“实时”写入这些输出是否至关重要,或者正在写入多少数据。根据您的限制,一种选择可能是缓冲所有输出并在软件运行结束时将它们写入。
一种变体是设置固定大小的内部缓冲区,一旦达到内部缓冲区限制,打开文件,追加和关闭,然后清除缓冲区以获得更多输出。缓冲区减少了打开/关闭周期的数量,并为您提供文件系统通常设置为很好处理的突发写入。这适用于您需要一些实时写入,和/或数据大于可用内存,并且文件句柄超过系统中的某个最大值的情况。
您可以分两步完成。
1) 将前 19 个 id 写入一个文件,接下来的 19 个 id 写入下一个文件,依此类推。因此,您需要为此步骤并行打开 8 个输出文件(和输入文件)。
2)为每个这样创建的文件创建 19 个(最后一个只有 13 个)新文件并将 ID 写入其中。
无论输入文件有多大以及它包含多少个 id-datasets,您总是需要打开和关闭 163 个文件。但是您需要将数据写入两次,因此只有在 id-datasets 非常小且随机分布的情况下才值得。
我认为在大多数情况下,更频繁地打开和关闭文件会更有效。
最安全的方法是在写入后打开文件并刷新,如果最近没有写入,则关闭。您的程序无法控制的许多事情可能会损坏文件的内容。继续阅读时请记住这一点。
我建议保留一个std::map
or std::vector
ofFILE
指针。允许您通过map
ID 访问文件指针。如果 ID 范围很小,您可以创建一个vector
, 保留元素,并使用 ID 作为索引。这将允许您同时打开许多文件。当心数据损坏的概念。
同时打开文件的限制由操作系统设置。例如,如果您的操作系统最多有 10 个,则您将在请求第 11 个文件时进行安排。
另一个技巧是在动态内存中为每个文件保留缓冲区。处理完所有数据后,打开一个文件(或多个文件),写入缓冲区(使用一个fwrite
),关闭并继续。这可能会更快,因为您在数据处理期间写入内存而不是文件。一个有趣的旁注是,您的操作系统也可能将缓冲区分页到硬盘驱动器。缓冲区的大小和数量是一个依赖于平台的优化问题(您必须调整和测试才能获得良好的组合)。如果操作系统将内存分页到磁盘,您的程序将变慢。
好吧,如果我在 OP 中使用您列出的约束来编写它,我将创建 146 个缓冲区并将数据放入其中,然后最后依次遍历缓冲区并关闭/打开单个文件句柄。
您在评论中提到,速度是一个主要问题,而天真的方法太慢了。
您可以开始考虑一些事情。一种是将二进制文件重新组织成顺序条,这将允许并行操作。另一种是最近最少使用的文件句柄集合方法。另一种方法可能是分叉到 8 个不同的进程,每个进程输出 19-20 个文件。
根据二进制组织(高度碎片化与高度顺序),其中一些方法或多或少会更实用。
一个主要的限制是二进制数据的大小。它比缓存大吗?比内存还大?从磁带机中流出?不断脱离传感器流并且仅作为内存中的“文件”存在?每一个都提出了不同的优化策略......
另一个问题是使用模式。您是偶尔对文件进行峰值写入,还是只写入了几次大块?这决定了文件句柄的不同缓存/分页策略的有效性。
假设您在 *nix 系统上,限制是每个进程,而不是系统范围。这意味着您可以启动多个进程,每个进程负责您要过滤的 id 的一个子集。每个进程都可以保持在 FOPEN_MAX 内。
您可以让一个父进程读取输入文件,然后通过管道特殊文件将数据发送到各种“写入”进程。
要实现最少的文件打开和关闭次数,您必须多次通读输入。每次,您选择需要排序的 id 子集,然后仅将这些记录提取到输出文件中。
每个线程的伪代码:
fseek()
回到输入的开头。fseek()
到输入的开头。这种方法不能很好地用于多个线程,因为最终线程将读取文件的完全不同部分。发生这种情况时,文件缓存很难高效。您可以使用障碍来或多或少地保持线程同步。
您可以使用多个线程和一个大型缓冲池来只运行一次输入。这是以打开和关闭更多文件为代价的(可能)。每个线程都会,直到整个文件被排序:
fwrite()
。如果没有,请等到它最低(希望这种情况不会发生太多)。您可以更改将输出文件刷新到磁盘的单位。也许您有足够的 RAM 一次收集 200 页,每个输出文件?
需要注意的事项:
fwrite()
同时连接到同一个输出文件。如果发生这种情况,您可能会损坏其中一个页面。