【问题标题】:why python dict update insanely slow?为什么 python dict 更新非常慢?
【发布时间】:2017-03-02 08:23:40
【问题描述】:

我有一个 python 程序,它从文件中读取行并将它们放入 dict,简单来说,它看起来像:

data = {'file_name':''}
with open('file_name') as in_fd:
    for line in in_fd:
        data['file_name'] += line

我发现它需要几个小时才能完成。

然后,我对程序做了一点改动:

data = {'file_name':[]}
with open('file_name') as in_fd:
    for line in in_fd:
        data['file_name'].append(line)
    data['file_name'] = ''.join(data['file_name'])

它在几秒钟内完成。

我以为是+= 使程序变慢,但似乎不是。请看下面的测试结果。

我知道我们可以使用列表追加和连接来提高连接字符串时的性能。但是我从没想过append and joinadd and assign之间会有这么大的性能差距。

所以我决定再做一些测试,最后发现是dict更新操作让程序变慢了。这是一个脚本:

import time
LOOPS = 10000
WORD = 'ABC'*100

s1=time.time()
buf1 = []
for i in xrange(LOOPS):
    buf1.append(WORD)
ss = ''.join(buf1)

s2=time.time()
buf2 = ''
for i in xrange(LOOPS):
    buf2 += WORD

s3=time.time()
buf3 = {'1':''}
for i in xrange(LOOPS):
    buf3['1'] += WORD

s4=time.time()
buf4 = {'1':[]}
for i in xrange(LOOPS):
    buf4['1'].append(WORD)
buf4['1'] = ''.join(buf4['1'])

s5=time.time()
print s2-s1, s3-s2, s4-s3, s5-s4

在我的笔记本电脑(mac pro 2013 mid, OS X 10.9.5, cpython 2.7.10)中,它的输出是:

0.00299620628357 0.00415587425232 3.49465799332 0.00231599807739

受 juanpa.arrivillaga 的 cmets 启发,我对第二个循环做了一点改动:

trivial_reference = []
buf2 = ''
for i in xrange(LOOPS):
    buf2 += WORD
    trivial_reference.append(buf2)  # add a trivial reference to avoid optimization

更改后,现在第二个循环需要 19 秒才能完成。所以就像 juanpa.arrivillaga 说的那样,这似乎只是一个优化问题。

【问题讨论】:

  • 你应该做一个连接字符串然后分配给一个字典而不是+=一个字典上的字符串。
  • 最好使用timeit 模块进行此类基准测试。
  • 这与update 无关,但如果对给定字符串只有一个引用,则可能是某种优化。将第三个循环改为x = buf3.pop('1'); x += WORD; buf3['1'] = x,结果差别不大:0.00136399269104 0.00481796264648 0.00717210769653 0.00203800201416
  • 它不仅仅是字典。使用列表buf5=['']buf5[0] += WORD,你会得到类似的缓慢。就是更新慢了。
  • @muru 是的,juanpa.arrivillaga 的评论可能是对的,这是一个优化问题,您的测试证明了这一点。

标签: python performance dictionary cpython python-internals


【解决方案1】:

+= 在构建大字符串时表现非常糟糕,但在 CPython 中的一种情况下可能很高效。下面提到

为了确保更快的字符串连接,请使用str.join()


来自Python Performance Tips下的String Concatenation部分:

避免这种情况:

s = ""
for substring in list:
    s += substring

请改用s = "".join(list)。在构建大字符串时,前者是一个非常常见的灾难性错误。


为什么s += xs['1'] += xs[0] += x 快?

From Note 6:

CPython 实现细节:如果 s 和 t 都是字符串,一些 诸如 CPython 之类的 Python 实现通常可以就地执行 优化s = s + ts += t 形式的赋值。什么时候 适用,这种优化使二次运行时间大大减少 可能。这个优化既是版本也是实现 依赖。对于性能敏感的代码,最好使用 str.join() 确保一致的线性连接的方法 跨版本和实现的性能。

CPython 的优化是如果一个字符串只有一个引用,那么我们可以resize it in-place

/* 请注意,我们不必为非共享 Unicode 修改 *unicode 对象,因为我们可以就地修改它们。 */

现在后两个不是简单的就地添加。事实上,这些根本不是就地添加。

s[0] += x

相当于:

temp = s[0]  # Extra reference. `S[0]` and `temp` both point to same string now.
temp += x
s[0] = temp

例子:

>>> lst = [1, 2, 3]
>>> def func():
...     lst[0] = 90
...     return 100
...
>>> lst[0] += func()
>>> print lst
[101, 2, 3]  # Not [190, 2, 3]

但一般不要使用s += x 连接字符串,始终使用str.join 连接字符串集合。


时间

LOOPS = 1000
WORD = 'ABC'*100


def list_append():
    buf1 = [WORD for _ in xrange(LOOPS)]
    return ''.join(buf1)


def str_concat():
    buf2 = ''
    for i in xrange(LOOPS):
        buf2 += WORD


def dict_val_concat():
    buf3 = {'1': ''}
    for i in xrange(LOOPS):
        buf3['1'] += WORD
    return buf3['1']


def list_val_concat():
    buf4 = ['']
    for i in xrange(LOOPS):
        buf4[0] += WORD
    return buf4[0]


def val_pop_concat():
    buf5 = ['']
    for i in xrange(LOOPS):
        val = buf5.pop()
        val += WORD
        buf5.append(val)
    return buf5[0]


def val_assign_concat():
    buf6 = ['']
    for i in xrange(LOOPS):
        val = buf6[0]
        val += WORD
        buf6[0] = val
    return buf6[0]


>>> %timeit list_append()
1000 loops, best of 3: 1.31 ms per loop
>>> %timeit str_concat()
100 loops, best of 3: 3.09 ms per loop
>>> %run so.py
>>> %timeit list_append()
10000 loops, best of 3: 71.2 us per loop
>>> %timeit str_concat()
1000 loops, best of 3: 276 us per loop
>>> %timeit dict_val_concat()
100 loops, best of 3: 9.66 ms per loop
>>> %timeit list_val_concat()
100 loops, best of 3: 9.64 ms per loop
>>> %timeit val_pop_concat()
1000 loops, best of 3: 556 us per loop
>>> %timeit val_assign_concat()
100 loops, best of 3: 9.31 ms per loop

val_pop_concat 在这里很快,因为通过使用pop(),我们正在从列表中删除对该字符串的引用,现在 CPython 可以就地调整它的大小(@niemmi in comments 正确猜测)。

【讨论】:

  • 我不这么认为,s3-s2 很小,s4-3 在我的测试中要大得多。
  • @WKPlus 所以,这是一个实现细节,最近版本的 CPython(我不记得确切,类似 >2.6)优化了字符串的 += 分配,否则它是 i> 二次时间运算。在s2 中,我认为解释器能够优化,但在s3 中却没有,可能是因为您将字符串作为字典值访问。
  • 真的希望通过分析intepreter code 并证明在一种情况下先决条件(引用计数等)得到满足,而在第二种情况下没有得到满足。结果观察到的行为是优化或未优化的字符串连接。
  • @juanpa.arrivillaga 我又做了一个测试,添加了一个对临时字符串的简单引用,发现它会减慢程序的速度。
  • @ŁukaszRogalski 和dis 一起看也很有启发性。
猜你喜欢
  • 2013-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-11
  • 1970-01-01
  • 1970-01-01
  • 2021-12-03
  • 2011-10-19
相关资源
最近更新 更多