Python mmap - 读取文件末尾速度慢 [含测试代码]

5

几天前我发布了一个类似的问题,但没有任何代码,现在我创建了一个测试代码,希望能得到一些帮助。

代码在底部。

我有一些数据集,其中有很多大文件(约100个),我想非常高效地提取这些文件中的特定行(在内存和速度方面都要高效)。

我的代码获取相关文件的列表,代码打开每个文件并使用[line 2]将文件映射到内存中,此外,对于每个文件,我收到一组索引并在索引上依次检索相关信息(例如此示例中的10字节):[line 3-4],最后我使用[line 5-6]关闭处理程序。

binaryFile = open(path, "r+b")
binaryFile_mm = mmap.mmap(binaryFile.fileno(), 0)
for INDEX in INDEXES:
    information = binaryFile_mm[(INDEX):(INDEX)+10].decode("utf-8")
binaryFile_mm.close()
binaryFile.close()

这段代码并行运行,每个文件都有数千个索引,并且每秒钟执行多次,持续数小时。
现在遇到的问题是,当我限制索引范围较小时(也就是说,当我要求代码从文件开头获取信息时),它能够正常运行。但是!当我增加索引范围时,一切都会变得非常缓慢,buff/cache内存也会被填满(我不确定内存问题是否与减速有关)。
我的问题是,为什么从文件的开头或结尾检索信息很重要,如何克服它以便从文件末尾获得即时访问信息而不会减速和增加buff/cache内存使用。
PS - 一些数字和大小:我有大约100个1GB大小的文件,当我将索引限制为文件0%-10%时,它能够正常运行,但当我允许索引位于文件中的任何位置时,它就无法工作了。
代码 - 在Linux和Windows上测试过Python 3.5,需要10 GB的存储空间(创建了3个文件,每个文件内部有3GB的随机字符串)。
import os, errno, sys
import random, time
import mmap



def create_binary_test_file():
    print("Creating files with 3,000,000,000 characters, takes a few seconds...")
    test_binary_file1 = open("test_binary_file1.testbin", "wb")
    test_binary_file2 = open("test_binary_file2.testbin", "wb")
    test_binary_file3 = open("test_binary_file3.testbin", "wb")
    for i in range(1000):
        if i % 100 == 0 :
            print("progress -  ", i/10, " % ")
        # efficiently create random strings and write to files
        tbl = bytes.maketrans(bytearray(range(256)),
                          bytearray([ord(b'a') + b % 26 for b in range(256)]))
        random_string = (os.urandom(3000000).translate(tbl))
        test_binary_file1.write(str(random_string).encode('utf-8'))
        test_binary_file2.write(str(random_string).encode('utf-8'))
        test_binary_file3.write(str(random_string).encode('utf-8'))
    test_binary_file1.close()
    test_binary_file2.close()
    test_binary_file3.close()
    print("Created binary file for testing.The file contains 3,000,000,000 characters")




# Opening binary test file
try:
    binary_file = open("test_binary_file1.testbin", "r+b")
except OSError as e: # this would be "except OSError, e:" before Python 2.6
    if e.errno == errno.ENOENT: # errno.ENOENT = no such file or directory
        create_binary_test_file()
        binary_file = open("test_binary_file1.testbin", "r+b")




## example of use - perform 100 times, in each itteration: open one of the binary files and retrieve 5,000 sample strings
## (if code runs fast and without a slowdown - increase the k or other numbers and it should reproduce the problem)

## Example 1 - getting information from start of file
print("Getting information from start of file")
etime = []
for i in range(100):
    start = time.time()
    binary_file_mm = mmap.mmap(binary_file.fileno(), 0)
    sample_index_list = random.sample(range(1,100000-1000), k=50000)
    sampled_data = [[binary_file_mm[v:v+1000].decode("utf-8")] for v in sample_index_list]
    binary_file_mm.close()
    binary_file.close()
    file_number = random.randint(1, 3)
    binary_file = open("test_binary_file" + str(file_number) + ".testbin", "r+b")
    etime.append((time.time() - start))
    if i % 10 == 9 :
        print("Iter ", i, " \tAverage time - ", '%.5f' % (sum(etime[-9:]) / len(etime[-9:])))
binary_file.close()


## Example 2 - getting information from all of the file
print("Getting information from all of the file")
binary_file = open("test_binary_file1.testbin", "r+b")
etime = []
for i in range(100):
    start = time.time()
    binary_file_mm = mmap.mmap(binary_file.fileno(), 0)
    sample_index_list = random.sample(range(1,3000000000-1000), k=50000)
    sampled_data = [[binary_file_mm[v:v+1000].decode("utf-8")] for v in sample_index_list]
    binary_file_mm.close()
    binary_file.close()
    file_number = random.randint(1, 3)
    binary_file = open("test_binary_file" + str(file_number) + ".testbin", "r+b")
    etime.append((time.time() - start))
    if i % 10 == 9 :
        print("Iter ", i, " \tAverage time - ", '%.5f' % (sum(etime[-9:]) / len(etime[-9:])))
binary_file.close()

我的结果:(从整个文件中获取信息的平均时间几乎比从开头获取信息慢了4倍,当有大约100个文件和并行计算时,这种差异会变得更加明显)

Getting information from start of file
Iter  9         Average time -  0.14790
Iter  19        Average time -  0.14590
Iter  29        Average time -  0.14456
Iter  39        Average time -  0.14279
Iter  49        Average time -  0.14256
Iter  59        Average time -  0.14312
Iter  69        Average time -  0.14145
Iter  79        Average time -  0.13867
Iter  89        Average time -  0.14079
Iter  99        Average time -  0.13979
Getting information from all of the file
Iter  9         Average time -  0.46114
Iter  19        Average time -  0.47547
Iter  29        Average time -  0.47936
Iter  39        Average time -  0.47469
Iter  49        Average time -  0.47158
Iter  59        Average time -  0.47114
Iter  69        Average time -  0.47247
Iter  79        Average time -  0.47881
Iter  89        Average time -  0.47792
Iter  99        Average time -  0.47681

@Danny_ds 对于混淆感到抱歉,我的意思是总体上我有数百个不同的文件将被访问。我使用合理数量的线程在4-16之间。 - artembus
2个回答

2
你有时间差的基本原因是需要在文件中寻找所需位置。距离位置0越远,所需时间就越长。如果你知道需要的起始索引,可以在文件描述符上定位到该点,然后进行mmap操作,这样可能会有所帮助。或者,为什么不直接从定位到的位置读取所需字节数,并将其放入结果变量中呢?最初的回答

这是我对mmap的印象 - 我认为它直接从内存中读取信息而不需要寻找(内存映射)。根据我的先前研究,看起来我可以通过在Python中使用mmap获得即时随机访问内存,那么我该如何使用mmap来实现您的解决方案呢?(我现在进行了简短的谷歌搜索,但没有找到太相似的东西,这对于一个默认的Python包来说很奇怪...) - artembus
一旦将文件映射到内存中,确实可以立即访问。但在此之前,内核仍然需要执行相同的操作,以便将您想要的数据放入地址空间。您系统的mmap实现可能默认只映射文件的第一页。您可以在创建映射时使用不同的页面大小,但这可能会以不希望的方式影响性能 :| - James McPherson
既然你提到了页面,我实际上确实尝试过测试加载特定页面以及读取信息的方式,但是我没有看到任何性能上的变化,也许是我的代码写错了。 - artembus
“你距离位置0越远,花费的时间就会越长。”这导致了另一个问题(https://dev59.com/T7Xna4cB1Zd3GeqPNpv0),我对此持不同意见。我刚刚在这里发布了一个单独的答案,阐述了性能差异背后的原因。 - Nickolay

1
要确定您是否获得足够的性能,请检查缓冲/页面缓存可用的内存(在Linux中为free),I/O统计信息-读取数量、大小和持续时间(iostat;与您的硬件规格进行比较),以及您的进程的CPU利用率。

[编辑] 假设您从本地连接的SSD中读取(没有将需要的数据存储在缓存中):

  • 当在单线程中读取时,您应该预计您的50,000个读取批次需要超过7秒钟(50000 * 0.000150)。可能需要更长时间,因为mmap文件的50k访问将触发更多或更大的读取,因为您的访问不是页面对齐的 - 正如我在另一个Q&A中建议的那样,我会改用简单的seek/read(并使用buffering=0打开文件以避免Python缓冲I/O的不必要读取)。
  • 使用更多线程/进程同时读取,您可以饱和SSD吞吐量(它可以执行多少4KB读取/秒 - 可以从5,000到1,000,000),然后单个读取将变得更慢。

[/编辑]

第一个示例仅访问文件数据的3 * 100KB,因此由于可用于缓存的数据远远超过这个数量,所有300KB很快就会进入缓存,因此您将看不到I/O,并且您的python进程将受到CPU限制。

我99.99%确定,如果您测试每个文件的最后100KB的读取,它将与第一个示例一样表现良好 - 这与数据的位置无关,而是与所访问的数据的大小有关。
第二个示例从9GB的随机部分访问,因此只有在您拥有足够的空闲RAM来缓存所有9GB,并且仅在您预加载文件到缓存中后,才能希望看到类似的性能,这样测试用例就可以在零I/O下运行。
在实际场景中,文件不会完全在缓存中 - 因此您将看到许多I/O请求和Python的更低CPU利用率。由于I/O比缓存访问慢得多,因此您应该期望此示例运行较慢。

此外,由于我的脚本在访问距离起始点更远的信息时并不会逐渐变慢,而是在文件足够大后显著下降 - 你的解释更有道理。这意味着在某个点之前,我的内存足以加载数据,但在那个点之后,内存不足,需要进行io操作,从而减慢了进程。但这正是我的全部观点...我想从一个大的二进制文件中仅读取小部分,并且我有这些部分的确切索引,但似乎在Python中无法做到这一点而不加载整个文件。 - artembus
1
如果您正在尝试测试“冷缓存”性能,其中大部分内容都从磁盘读取,那么130 tps太低了-您应该清除页面缓存。您最后的评论听起来像是想要从磁盘读取数据而不必等待它从磁盘中读取-我在我的答案中添加了一个[编辑],其中包含有关访问磁盘时应该期望读取速度有多慢的信息。 - Nickolay
1
清除缓存并不是为了“提高”性能,而只是为了使测量结果稳定。在磁盘缓存存在的情况下,性能会因缓存内容而变化很大(从毫秒到秒),这会让事情变得混乱。一次读取50MB的请求肯定比进行50000个请求要快得多。在SSD上,许多并行进程的总吞吐量可能是相同的,我不确定您当前的性能在这种情况下有多糟糕,以及您的I/O子系统有多快 - 您可以期望典型SSD的速度约为1GB/s。 - Nickolay
我有一个后续问题,当我运行脚本(加载数据并对其执行一些分析)时,我注意到它开始以相当快的速度运行。但是随着时间的推移,几个小时后,每个“周期”的原始运行时间会逐渐变慢,最多减少到X2、X3、X4倍,并且越来越糟糕。你有什么想法吗?如果我完全停止脚本并将PC留给它几分钟,然后再运行脚本-脚本开始表现良好,但逐渐变慢。我尝试了简单的CTRL+Z几分钟,但没有帮助。 - artembus
1
不,我没有,但是它似乎与页面缓存无关,因为重新启动和暂停进程的行为不同。 - Nickolay
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接