【发布时间】:2014-06-04 14:28:25
【问题描述】:
问题
为什么在 CPython 中会这样做
def add_string(n):
s = ''
for _ in range(n):
s += ' '
需要线性时间,但是
def add_string_in_list(n):
l = ['']
for _ in range(n):
l[0] += ' '
采用二次方时间?
证明:
Timer(partial(add_string, 1000000)).timeit(1)
#>>> 0.1848409200028982
Timer(partial(add_string, 10000000)).timeit(1)
#>>> 1.1123797750042286
Timer(partial(add_string_in_list, 10000)).timeit(1)
#>>> 0.0033865350123960525
Timer(partial(add_string_in_list, 100000)).timeit(1)
#>>> 0.25131178900483064
我知道的
当要添加的字符串的引用计数为 1 时,CPython 对字符串添加进行了优化。
这是因为 Python 中的字符串是不可变的,因此通常无法对其进行编辑。如果一个字符串存在多个引用并且它发生了变异,both 引用将看到更改后的字符串。这显然是不希望的,因此多个引用不会发生突变。
但是,如果只有一个对字符串的引用,则更改值只会更改希望更改的那个引用的字符串。您可以测试这是否是可能的原因:
from timeit import Timer
from functools import partial
def add_string_two_references(n):
s = ''
for _ in range(n):
s2 = s
s += ' '
Timer(partial(add_string_two_references, 20000)).timeit(1)
#>>> 0.032532954995986074
Timer(partial(add_string_two_references, 200000)).timeit(1)
#>>> 1.0898985149979126
我不确定为什么这个因子只有 30 倍,而不是预期的 100 倍,但我相信这是开销。
我不知道的事情
那么为什么列表版本会创建两个引用?这甚至是阻止优化的原因吗?
您可以检查它对普通对象的处理方式是否有所不同:
class Counter:
def __iadd__(self, other):
print(sys.getrefcount(self))
s = Counter()
s += None
#>>> 6
class Counter:
def __iadd__(self, other):
print(sys.getrefcount(self))
l = [Counter()]
l[0] += None
#>>> 6
【问题讨论】:
-
"如果一个字符串存在多个引用并且它发生了变化,那么这两个引用都会看到变化的字符串。"这不是真的。
-
“它改变了”≜“变异了”;我会修正措辞。 // 固定。
-
没有。
x = 'a'; y = x; x = 'b'- 和y仍然持有“a”。x = 'a'; y = x; x += 'b'也一样 -
我同意。但在那种情况下,字符串不会发生变异。 CPython 仅在安全的情况下(当只有一个引用时)对字符串进行变异;否则
y会改变,这很糟糕。 -
虽然
+=是一个特殊的部分优化案例,但推荐的方式是使用.join(),保证跨实现优化,并得到开发者的喜爱。
标签: python string optimization cpython