我目前有一个小型项目,我想在其中尽快对计算机上的20GB文件进行排序。想法是对文件进行分块,对块进行排序,合并块。我只是使用pyenv
为不同Python版本的radixsort代码计时,并且看到2.7.18
比3.6.10
,3.7.7
,3.8.3
和3.9.0a
快得多。在这个简单的示例中,有人可以解释为什么Python 3.x比2.7.18慢吗?是否添加了新功能?
import os
def chunk_data(filepath, prefixes):
"""
Pre-sort and chunk the content of filepath according to the prefixes.
Parameters
----------
filepath : str
Path to a text file which should get sorted. Each line contains
a string which has at least 2 characters and the first two
characters are guaranteed to be in prefixes
prefixes : List[str]
"""
prefix2file = {}
for prefix in prefixes:
chunk = os.path.abspath("radixsort_tmp/{:}.txt".format(prefix))
prefix2file[prefix] = open(chunk, "w")
# This is where most of the execution time is spent:
with open(filepath) as fp:
for line in fp:
prefix2file[line[:2]].write(line)
执行时间(多次运行):
完整的代码是on Github以及minimal complete version
统一码
是的,我知道Python 3和Python 2处理字符串的方式有所不同。我尝试以二进制模式(
rb
/wb
)打开文件,请参见“二进制模式”注释。在几次运行中,它们的速度要快一点。尽管如此,Python 2.7在所有运行中都更快。尝试1:访问字典
当我说出这个问题时,我认为词典访问可能是造成这种差异的原因。但是,我认为字典访问的总执行时间比I/O的总执行时间少。另外,timeit没有显示任何重要信息:
import timeit
import numpy as np
durations = timeit.repeat(
'a["b"]',
repeat=10 ** 6,
number=1,
setup="a = {'b': 3, 'c': 4, 'd': 5}"
)
mul = 10 ** -7
print(
"mean = {:0.1f} * 10^-7, std={:0.1f} * 10^-7".format(
np.mean(durations) / mul,
np.std(durations) / mul
)
)
print("min = {:0.1f} * 10^-7".format(np.min(durations) / mul))
print("max = {:0.1f} * 10^-7".format(np.max(durations) / mul))
尝试2:复制时间
作为一个简化的实验,我尝试复制20GB的文件:
通过shell的
cp
:230s Python的东西是由以下代码生成的。
我首先想到的是差异很大。因此,这可能是原因。但是,
chunk_data
执行时间的差异也很大,但是Python 2.7的平均值明显低于Python3.x。因此,这似乎不是像我在这里尝试的那样简单的I/O方案。import time
import sys
import os
version = sys.version_info
version = "{}.{}.{}".format(version.major, version.minor, version.micro)
if os.path.isfile("numbers-tmp.txt"):
os.remove("numers-tmp.txt")
t0 = time.time()
with open("numbers-large.txt") as fin, open("numers-tmp.txt", "w") as fout:
for line in fin:
fout.write(line)
t1 = time.time()
print("Python {}: {:0.0f}s".format(version, t1 - t0))
我的系统
最佳答案
这是多种效果的组合,多数情况是Python 3在文本模式下工作时需要执行unicode解码/编码,而在二进制模式下,它将通过专用的缓冲IO实现发送数据。
首先,使用 time.time
来衡量执行时间会消耗时间,因此包括了各种与Python不相关的事情,例如OS级缓存和缓冲以及存储介质的缓冲。它还反射(reflect)了对需要存储介质的其他过程的任何干扰。这就是为什么您会在计时结果中看到这些疯狂的变化。这是我的系统的结果,每个版本连续运行七次:
py3 = [660.9, 659.9, 644.5, 639.5, 752.4, 648.7, 626.6] # 661.79 +/- 38.58
py2 = [635.3, 623.4, 612.4, 589.6, 633.1, 613.7, 603.4] # 615.84 +/- 15.09
尽管差异很大,但似乎这些结果确实表明了不同的时间,例如可以通过统计检验来确认:
>>> from scipy.stats import ttest_ind
>>> ttest_ind(p2, p3)[1]
0.018729004515179636
即只有2%的机率会出现在同一分布中。
通过测量处理时间而不是墙面时间,我们可以获得更精确的图像。在Python 2中,这可以通过
time.clock
完成,而Python 3.3+提供 time.process_time
。这两个功能报告以下时间:py3_process_time = [224.4, 226.2, 224.0, 226.0, 226.2, 223.7, 223.8] # 224.90 +/- 1.09
py2_process_time = [171.0, 171.1, 171.2, 171.3, 170.9, 171.2, 171.4] # 171.16 +/- 0.16
现在,由于时序仅反射(reflect)了Python进程,因此数据中的分布少得多。
这些数据表明,Python 3的执行时间大约需要53.7秒。给定输入文件中的大量行(
550_000_000
),每次迭代总计约97.7纳秒。导致执行时间增加的第一个结果是Python 3中的unicode字符串。从文件中读取二进制数据,对其进行解码,然后在回写时再次对其进行编码。在Python 2中,所有字符串都立即存储为二进制字符串,因此不会带来任何编码/解码开销。您在测试中看不到这种效果,因为在各种外部资源引入的巨大变化中,这种影响消失了,这反射(reflect)在墙时差中。例如,我们可以测量从二进制到unicode到二进制的往返时间:
In [1]: %timeit b'000000000000000000000000000000000000'.decode().encode()
162 ns ± 2 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
这确实包括两个属性查找以及两个函数调用,因此所需的实际时间小于上面报告的值。要查看对执行时间的影响,我们可以将测试脚本更改为使用二进制模式
"rb"
和"wb"
而不是文本模式"r"
和"w"
。这样可以减少Python 3的计时结果,如下所示:py3_binary_mode = [200.6, 203.0, 207.2] # 203.60 +/- 2.73
每次迭代可将处理时间减少约21.3秒或38.7纳秒。这与往返基准测试的计时结果减去名称查找和函数调用的计时结果一致:
In [2]: class C:
...: def f(self): pass
...:
In [3]: x = C()
In [4]: %timeit x.f()
82.2 ns ± 0.882 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
In [5]: %timeit x
17.8 ns ± 0.0564 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each)
在这里,
%timeit x
度量了解析全局名称x
的额外开销,因此属性查找和函数调用使82.2 - 17.8 == 64.4
秒。从上面的往返数据中减去此开销两次,则为162 - 2*64.4 == 33.2
秒。现在,使用二进制模式的Python 3和Python 2之间仍然存在32.4秒的差异。这是由于Python 3中的所有IO都经过
io.BufferedWriter
的 .write
的(相当复杂的)实现,而在Python 2中 file.write
method进行得非常简单到 fwrite
。我们可以在两种实现中检查文件对象的类型:
$ python3.8
>>> type(open('/tmp/test', 'wb'))
<class '_io.BufferedWriter'>
$ python2.7
>>> type(open('/tmp/test', 'wb'))
<type 'file'>
在这里,我们还需要注意,Python 2的上述计时结果是通过使用文本模式而不是二进制模式获得的。二进制模式旨在支持实现buffer的所有对象protocol,这也会导致对字符串也执行其他工作(另请参见this question)。如果我们也针对Python 2切换到二进制模式,则可以获得:
py2_binary_mode = [212.9, 213.9, 214.3] # 213.70 +/- 0.59
实际上比Python 3的结果要大一点(18.4 ns/迭代)。
两种实现在其他细节上也有所不同,例如
dict
实现。为了衡量这种效果,我们可以创建一个相应的设置:from __future__ import print_function
import timeit
N = 10**6
R = 7
results = timeit.repeat(
"d[b'10'].write",
setup="d = dict.fromkeys((str(i).encode() for i in range(10, 100)), open('test', 'rb'))", # requires file 'test' to exist
repeat=R, number=N
)
results = [x/N for x in results]
print(['{:.3e}'.format(x) for x in results])
print(sum(results) / R)
这为Python 2和Python 3提供了以下结果:
对于整个550M迭代,此大约21.2纳秒的额外差异大约为12秒。
上面的时序代码仅检查dict查找是否只有一个键,因此我们还需要验证是否没有哈希冲突:
$ python3.8 -c "print(len({str(i).encode() for i in range(10, 100)}))"
90
$ python2.7 -c "print len({str(i).encode() for i in range(10, 100)})"
90
关于python - 自python 2.7以来,I/O变慢了吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62079732/