为什么同时读取多个文件比按顺序读取要慢?

时间:2017-03-06 07:58:42

标签: python performance python-2.7 io multiprocessing

我正在尝试解析目录中找到的许多文件,但是使用多处理会减慢我的程序。

# Calling my parsing function from Client.
L = getParsedFiles('/home/tony/Lab/slicedFiles') <--- 1000 .txt files found here.
                                                       combined ~100MB

从python文档中获取此示例:

from multiprocessing import Pool

def f(x):
    return x*x

if __name__ == '__main__':
    p = Pool(5)
    print(p.map(f, [1, 2, 3]))

我写了这段代码:

from multiprocessing import Pool
from api.ttypes import *

import gc
import os

def _parse(pathToFile):
    myList = []
    with open(pathToFile) as f:
        for line in f:
            s = line.split()
            x, y = [int(v) for v in s]
            obj = CoresetPoint(x, y)
            gc.disable()
            myList.append(obj)
            gc.enable()
    return Points(myList)

def getParsedFiles(pathToFile):
    myList = []
    p = Pool(2)
    for filename in os.listdir(pathToFile):
        if filename.endswith(".txt"):
            myList.append(filename)
    return p.map(_pars, , myList)

我按照示例,将以.txt结尾的文件的所有名称放在列表中,然后创建池,并将它们映射到我的函数。然后我想返回一个对象列表。每个对象保存文件的已解析数据。然而,令我惊讶的是,我得到了以下结果:

#Pool 32  ---> ~162(s)
#Pool 16 ---> ~150(s)
#Pool 12 ---> ~142(s)
#Pool 2 ---> ~130(s)

图形:
enter image description here

机器规格:

62.8 GiB RAM
Intel® Core™ i7-6850K CPU @ 3.60GHz × 12   

我在这里想念的是什么? 提前谢谢!

2 个答案:

答案 0 :(得分:10)

看起来像你I/O bound

  

在计算机科学中,I / O界限是指完成计算所花费的时间主要取决于等待输入/输出操作完成所花费的时间的条件。这与CPU绑定的任务相反。当请求数据的速率低于其消耗速率时,或者换句话说,请求数据所花费的时间多于处理数据的时间,就会出现这种情况。

当子进程可用时,您可能需要让主线程执行读取并将数据添加到池中。这与使用map

不同

当您一次处理一行,并且输入被拆分时,您可以使用fileinput迭代多个文件的行,并映射到函数处理行而不是文件:

一次传递一行可能太慢,所以我们可以要求地图传递块,并且可以调整直到找到一个最佳点。我们的函数解析了大量的行:

def _parse_coreset_points(lines):
    return Points([_parse_coreset_point(line) for line in lines])

def _parse_coreset_point(line):
    s = line.split()
    x, y = [int(v) for v in s]
    return CoresetPoint(x, y)

我们的主要功能:

import fileinput

def getParsedFiles(directory):
    pool = Pool(2)

    txts = [filename for filename in os.listdir(directory):
            if filename.endswith(".txt")]

    return pool.imap(_parse_coreset_points, fileinput.input(txts), chunksize=100)

答案 1 :(得分:6)

一般来说,同时从不同线程读取相同的物理(旋转)硬盘永远不是一个好主意,因为每个开关都会导致大约10ms的额外延迟来定位硬盘的读头(会有所不同)在SSD)。

正如@ peter-wood所说,最好让一个线程读入数据,并让其他线程处理这些数据。

另外,为了真正测试差异,我认为你应该用一些更大的文件进行测试。例如:当前的硬盘应该能够读取大约100MB /秒。因此,一次读取100kB文件的数据需要1ms,而将读取头定位到该文件的开头则需要10ms。

另一方面,查看您的数字(假设这些是单循环),很难相信I / O绑定是这里唯一的问题。总数据为100MB,从磁盘读取需要1秒钟加上一些开销,但您的程序需要130秒。我不知道这个数字是否与磁盘上的文件是冷的,或者是平均多次测试,其中数据已经被OS缓存(62 GB或RAM,所有这些数据应该第二次缓存) - 看到这两个数字会很有意思。

所以必须有别的东西。让我们仔细看看你的循环:

for line in f:
    s = line.split()
    x, y = [int(v) for v in s]
    obj = CoresetPoint(x, y)
    gc.disable()
    myList.append(obj)
    gc.enable()

虽然我不了解Python,但我的猜测是gc来电是问题所在。从磁盘读取的每一行都调用它们。我不知道这些调用有多昂贵(或者如果gc.enable()触发垃圾收集的情况),以及为什么只需append(obj)周围需要它们,但可能还有其他问题因为这个是多线程的:

假设gc对象是全局的(即不是本地线程),你可以这样:

thread 1 : gc.disable()
# switch to thread 2
thread 2 : gc.disable()
thread 2 : myList.append(obj)
thread 2 : gc.enable()
# gc now enabled!
# switch back to thread 1 (or one of the other threads)
thread 1 : myList.append(obj)
thread 1 : gc.enable()

如果线程数&lt; =核心数量,甚至不会进行任何切换,他们都会同时调用它。

此外,如果gc对象是线程安全的(如果它不是更糟糕的话,它会更糟)它必须做一些锁定才能安全地改变它的内部状态,会迫使所有其他线程等待。

例如,gc.disable()看起来像这样:

def disable()
    lock()  # all other threads are blocked for gc calls now
    alter internal data
    unlock()

由于gc.disable()gc.enable()在紧密循环中被调用,因此在使用多个线程时会影响性能。

因此,最好删除这些调用,或者将它们放在程序的开头和结尾(如果确实需要它们)(或者只在开始时禁用gc,不需要执行{{1}在退出程序之前)。

根据Python复制或移动对象的方式,使用gc可能稍微好一些。

因此,使用一个线程并且没有myList.append(CoresetPoint(x, y))调用在一个100MB文件上测试相同内容会很有趣。

如果处理时间超过读数(即不是I / O限制),请使用一个线程读取缓冲区中的数据(如果尚未缓存,则应在一个100MB文件上花费1或2秒),以及多个线程处理数据(但在紧密循环中仍然没有gc次调用)。

您不必将数据拆分为多个文件,以便能够使用线程。只需让它们处理同一文件的不同部分(即使使用14GB文件)。