【问题标题】:Python os.system call slow ~1/2 secondPython os.system 调用慢~1/2 秒
【发布时间】:2014-08-29 22:41:33
【问题描述】:

我正在编写一个脚本来查找两个不同文件树中的所有重复文件。该脚本工作正常,但它太慢而无法处理大量文件(> 1000)。使用 cProfile 分析我的脚本后发现,我的代码中的一行代码几乎占据了所有的执行时间。

该行是对 os.system() 的调用:

cmpout = os.system("cmp -s -n 10MiB %s %s" % (callA, callB));

如果我有 N 个相同的文件,此调用位于一个 for 循环中,该循环会被调用大约 N 次。平均执行时间为 0.53 秒

  ncalls  tottime  percall  cumtime  percall filename:lineno(function)        
  563  301.540    0.536  301.540    0.536 {built-in method system}

当然,这很快就会增加一千多个文件。我尝试用子进程模块的调用替换它来加速它:

cmpout = call("cmp -s -n 10MiB %s %s" % (callA, callB), shell=True); 

但这具有几乎相同的执行时间。我也尝试过减少 cmp 命令本身的字节限制,但这只会节省非常少的时间。

有什么办法可以加快速度吗?

我正在使用的完整功能:

def dirintersect(dirA, dirB):
    intersectionAB = []
    filesA = listfiles(dirA);
    filesB = listfiles(dirB);
    for (pathB, filenameB) in filesB:
        for (pathA, filenameA) in filesA:
            if filenameA == filenameB:
                callA = shlex.quote(os.path.join(pathA, filenameA));
                callB = shlex.quote(os.path.join(pathB, filenameB));
                cmpout = os.system("cmp -s -n 10MiB %s %s" % (callA, callB));
                #cmpout = call("cmp -s -n 10MiB %s %s" % (callA, callB), shell=True); 
                if cmpout is 0:
                    intersectionAB.append((filenameB, pathB, pathA))                
    return intersectionAB

更新:感谢所有反馈!我将尝试解决您的大多数 cmets 并提供更多信息。 @Iarsmans。你是绝对正确的,我的嵌套 for 循环与 n² 缩放我自己已经发现我可以通过使用字典或集合来做同样的事情并执行集合操作。但即使是这种“坏”算法的开销对于运行 os.system 所花费的时间也是微不足道的。对于每个文件名,实际的 if 子句大约触发一次(也就是说,我希望每个文件名只有一个重复项)。所以 os.system 只运行 N 次而不是 N² 次,但即使对于这个线性时间它也不够快。

@Iarsman 和@Alex Reynolds:我没有像您建议的那样选择散列解决方案的原因是,在我设想的用例中,我将较小的目录树与较大的目录树进行比较,并对目录中的所有文件进行散列较大的树需要很长时间(因为它可能是整个分区中的所有文件),而我只需要对一小部分文件进行实际比较。

@abarnert:我在 call 命令中使用 shell=True 的原因仅仅是因为我从 os.system 开始,然后读到使用 subprocess.call 更好,这也是在两者之间进行转换的方式。如果有更好的方法来运行 cmp 命令,我想知道。我 qoute 参数的原因是,当我刚刚在命令中传递 os.path.join 结果时,文件名中的空格存在问题。

谢谢你的建议,我改成if cmpout == 0

@Gabe:我不知道如何为 bash 命令计时,但我相信当我只运行命令时它的运行速度要比半秒快得多。

我说字节限制并不重要,因为当我将其更改为仅 10Kib 时,它会将我的测试运行的总执行时间更改为 290 秒,而不是大约 300 秒。我保留限制的原因是为了防止它比较非常大的文件(例如 1GiB 视频文件)。

更新 2: 我已遵循@abarnert 的建议并将调用更改为:

cmpout = call(["cmp", '-s', '-n', '10MiB', callA, callB])

我的测试场景的执行时间现在从 300 秒下降到 270 秒。还不够,但这是一个开始。

【问题讨论】:

  • 产生一个子进程总是相当缓慢。在 Python 中重新实现 cmp 可能会更快。您可以做的另一个优化是检查文件大小;如果它们不同,那么您就知道文件不同。
  • 你为什么要使用 shell?您没有将它用于任何事情,而且您显然是在浪费时间在 cmp 程序之上启动和退出 shell。当然,它让事情变得更加复杂(你必须quote 的论点)。我不知道这是否是你的问题,但为什么要这样做呢?只需call(['cmp', '-s', '-n', '10MiB', os.path.join(pathA, filenameA), os.path.join(pathB, filenameB)])
  • 哈希(例如 SHA1)每个被比较的文件。将每个散列和路径存储在字典中。如果您尝试再次存储哈希(如果密钥已存在),则存储路径和当前路径指向重复文件。
  • cmp 无论您是否通过 Python 运行它所花费的时间都相同吗?
  • 附带说明,cmpout is 0 几乎永远不会正确。您正在检查,不仅callsystem 返回0,而且Python 恰好只有一次0 实例被实习并为返回0 的任何内容返回该值。这对于 CPython 来说是正确的,除非您在构建时禁用小数优化,但为什么要依赖它呢?只需执行if cmpout == 0if not cmpout

标签: python performance os.system


【解决方案1】:

您使用了错误的算法来执行此操作。比较所有文件对需要 n 个文件的 Θ(n²) 时间,而您可以通过散列文件在线性时间内得到两个目录的交集:

from hashlib import sha512
import os
import os.path

def hash_file(fname):
    with open(fname) as f:
        return sha512(f.read()).hexdigest()

def listdir(d):
    return [os.path.join(d, fname) for fname in os.listdir(d)]

def dirintersect(d1, d2):
    files1 = {hash_file(fname): fname for fname in listdir(d1)}
    return [(files1[hash_file(fname)], fname) for fname in listdir(d2)
            if hash_file(fname) in files1]

此函数循环遍历第一个目录,存储由其 SHA-512 哈希索引的文件名,然后通过在从第一个目录构建的索引中存在具有相同哈希的文件来过滤第二个目录中的文件。一些明显的优化留给读者练习:)

该函数假定目录仅包含常规文件或指向这些文件的符号链接,它会一次性将文件读入内存(但这并不难修复)。

(SHA-512 实际上并不能保证文件的相等性,因此可以安装一个完整的比较作为备用措施,尽管您很难找到两个具有相同 SHA-512 的文件。)

【讨论】:

  • 打开文件而不关闭它们总是不好的,但是当您试图尽快通过数千个文件时这样做尤其糟糕。如果您在 Jython 或 IronPython 中运行此代码,您认为会发生什么? (实际上,会发生NameError,就像在 CPython 中一样,但大概您的意思是在 dirintersect 中本地定义 hash_file,对吧?)
  • 我相信 IronPython 会抱怨字典理解。除非您使用的是 2.7 测试版,否则 Jython 也是如此。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-08
  • 2013-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多