我有一些非常大(> 4 GB)的文件包含(数百万)固定长度的二进制记录。我希望(有效地)将它们连接到其他文件中的记录,方法是将指针(即64位记录号)写入特定偏移的记录中。
详细说明,我有一对(键,记录号)元组的列表,按键排序,我想对给定的一对文件(例如A和B)执行每个连接。迭代列表对并匹配向上键产生一个表示连接记录的(键,记录号A,记录号B)元组的列表(为简单起见假设1:1映射)。为了完成连接,我在概念上需要寻找列表中的每个A记录并在适当的偏移处写入相应的B记录号,反之亦然。我的问题是实际执行此操作的最快方法是什么?
由于连接记录列表按键排序,因此关联的记录号基本上是随机的。假设文件比操作系统磁盘缓存大得多,那么做一堆随机搜索和写入似乎效率极低。我已经尝试通过将A-> B和B-> A映射放在稀疏数组中来对记录数进行部分排序,并在每次内存不足时将最密集的条目集群刷新到磁盘。这有利于大大增加在更新其第一个指针后为集群缓存适当记录的机会。但是,即使在这一点上,通常更好的做一堆搜索和盲写,或者手动读取文件块,更新适当的指针,然后将块写回来?虽然前一种方法更简单,并且可以通过操作系统进行优化,以便进行最少的扇区读取(因为它知道扇区大小)和副本(它可以通过直接读入正确对齐的缓冲区来避免复制),但似乎它将导致极高的系统调用开销。
虽然我喜欢便携式解决方案(即使它涉及对广泛使用的库的依赖,例如Boost),但现代Windows和Linux是唯一的必备解决方案,因此我可以使用特定于操作系统的API (例如,CreateFile提示或分散/收集I / O)。然而,这可能涉及很多工作甚至尝试,所以我想知道是否有人可以告诉我是否值得努力。
答案 0 :(得分:4)
看起来你可以通过使用数据结构来解决这个问题。你有三个限制因素:
B+ Trees专门用于解决您在此处理的工作量问题。链接的维基百科文章中有几个实现链接。
本质上,B +树是二叉搜索树,除了节点组在一起组。这样,B +树不必一次寻找每个节点,而是一次只加载一个块。它保留了一些信息,以便知道在搜索中需要哪个块。
编辑:如果您需要按多个项目排序,您可以执行以下操作:
+--------+-------------+-------------+---------+
| Header | B+Tree by A | B+Tree by B | Records |
+--------+-------------+-------------+---------+
|| ^ | ^ | ^
|\------/ | | | |
\-------------------/ | |
| | |
\----------+----------/
即。每个键都有单独的B +树,以及一个单独的记录列表,指针存储在B +树中。
答案 1 :(得分:3)
我尝试通过将A-> B和B-> A映射放在稀疏数组中来对记录数进行部分排序,并在每次内存不足时将最密集的条目集群刷新到磁盘。 它似乎会产生极高的系统调用开销。
您可以使用对文件的内存映射访问来避免系统调用开销。 * {NIX上的mmap()和CreateFileMapping() on Windows。
将文件逻辑拆分为块,例如32MB。如果需要在块中更改某些东西,mmap()它,修改数据,如果需要,可以选择msync(),munmap()然后移动到下一个块。
这本来是我先尝试过的。操作系统会自动读取需要读取的内容(首次访问数据时),无论如何都会排队IO。
要记住的重要事项是真正的IO并不那么快。随机访问的性能方面限制因素是(1)每秒IO数(IOPS)存储可以处理的数量和(2)磁盘搜索次数。 (通常的IOPS在几百个范围内。通常的寻道延迟是3-5ms。)例如,存储可以读/写50MB / s:一秒内连续一个50MB的块。但是如果你试图修补字节方式的50MB文件,那么寻找时间就会破坏性能。即使只更新几个字节,也可以读取更多并写入更多内容。
要观察的另一个限制是操作系统的最大IO操作大小:它取决于存储,但大多数操作系统会将IO任务拆分为大于128K。如果限制与存储中的类似限制同步,则可以更改限制并且最佳。
还要记住存储空间。许多人忘记了存储通常只有一个。我在这里试图说,启动crapload线程对IO没有帮助,除非你有多个存储。即使是单CPU /内核也能够通过800读取IOPS和400写入IOPS限制轻松使RAID10饱和。 (但每个存储的专用线程至少在理论上是有道理的。)
希望有所帮助。这里的其他人经常提到我没有经验的Boost.Asio--但是值得一试。
P.S。坦率地说,我很想听到你的问题的其他(更多信息)回复。我已经好几次在船上,却没有机会真正开始。欢迎与IO优化相关的书籍/链接/等(无论平台如何);)
答案 2 :(得分:1)
我没有建立一个(密钥,记录号A,记录号B)的列表,而是省去了用于节省空间和刚建立的密钥(记录号A,记录号B)。我用A来排序那个表或文件,依次寻找每个A记录,写入B号,然后按B排序列表,依次寻找每个B记录,写下A号。
我正在进行非常类似的大型文件操作,这些新的机器非常快,不需要很长时间:
在具有3gb ram和32位Vista的廉价2.4gHz HP Pavilion上,使用Delphi库例程(而不是Win API)将300万个连续的1,008字节记录写入新文件需要56秒。
按顺序寻找文件中的每条记录,并在启动的计算机上使用Win API FileSeek / FileWrite写入8个字节需要136秒。这是300万次更新。立即重新运行相同的代码需要108秒,因为O / S有一些缓存的东西。
首先对记录偏移进行排序,然后依次更新文件,这是可行的方法。
答案 3 :(得分:1)
随机磁盘访问往往比顺序磁盘访问慢几个数量级。因此,选择那些在乍一看可能听起来非常低效的算法会很有用。例如,您可以尝试这样做:
创建您的连接索引,但不要使用它,只需将对列表(A索引,B索引)写出到磁盘文件。
按A索引对这个新文件对进行排序。使用专为外部排序设计的排序算法(虽然我自己没有尝试过,但是当我研究类似问题时,来自stxxl.sourceforge.net的STXXL库看起来很有希望)
按顺序遍历A记录文件和已排序的对列表。读取一个巨大的块,在内存中进行所有相关的更改,写出块。切勿再次触摸A记录文件的那一部分(因为您计划进行的更改按顺序进行)
返回,按B索引对对文件进行排序(再次使用外部排序)。使用它以相同的方式更新B记录文件。