为什么python中字符串连接的顺序会大大影响速度?

时间:2018-06-09 20:33:23

标签: python string performance

我刚通过调试代码发现了这个问题。我有一个消息列表作为字符串,我试图连接在一起,我想在每条消息的末尾添加一个换行符。

方法1:

total_str = ""
for m in messages:
    total_str = total_str + m + "\n"

这非常慢 - 在大约第100,000条消息之后,添加每条消息需要大约2-3秒,并且在第300,000条消息附近,此过程基本上停止了。

方法2:

total_str = ""
for m in messages:
    tmp = m + "\n"
    total_str = total_str + tmp

这种方法在不到一秒的时间内就完成了所有160万条消息的连接。

我想知道为什么第二种方法比第一种方法快得多?

3 个答案:

答案 0 :(得分:3)

a + b + c不是将abc加入单个字符串的单个操作。它是两个操作,t = a + bt + c,这意味着复制a 两次的内容;一次将a复制到t,再次将t复制到t + c的结果中。因为,在您的示例中,a是不断变长的字符串,所以最佳将每一步复制的数据量增加一倍。

最好的方法是避免+创建的所有临时str对象,并使用join

total_str = "\n".join(messages)

join直接对每个字符串进行操作,而不需要一次迭代地将它们附加到一个初始空字符串。 join通过扫描messages计算得到的字符串需要多长时间,为其分配足够的内存,然后依次将messages的每个元素的数据复制到第一位。时间。

答案 1 :(得分:1)

好吧,因为a = a + b + c被执行为a = (a + b) + c,所以可以看到计算顺序如下:

  • tmp_1 = a + b。这必须复制巨大的字符串a,因为字符串是不可变的。
  • a = tmp_1 + c。这必须复制(甚至更多)巨大的字符串tmp_1,因为字符串是不可变的。

因此,涉及到两个大型副本,而在第二个版本中,a = a + tmp(与第二个示例中一样),只有一个这样的副本是需要。后一种方法显然会更快。

答案 2 :(得分:1)

Python's strings是不可变且连续的。前者意味着它们不能被修改,而后者意味着它们被存储在存储器中的一个地方。这与例如不同a rope data structure,其中附加数据是一种廉价操作,只需要为最终形成一个新节点。这意味着连接操作必须每次都复制两个输入字符串,并且使用类似total_str = total_str + m + "\n"的内容,因为+left associative,复制所有{{1}两次。通常的解决方案是保留所有小字符串直到整个集合完成,并使用str.join一次执行连接。这只会复制每个组件字符串一次,而不是几何(与方形成比例)次数。另一个选择是,使用io.StringIO来构建缓冲区。这将为您提供类似文件的对象,有点像其他语言中的total_str,您可以从中提取最终字符串。我们还有writelines之类的操作可以接受迭代,因此可能根本不需要连接。

我猜测为什么第二个实现变得如此之快(不只是快两倍),是因为有些优化可以让CPython根本不允许执行左操作数的副本。 PyUnicode_Append似乎具有基于StringBuilder的精确优化,其中如果引用计数精确为1,则字符串从未被哈希处理,以及其他一些条件,它可以改变对象。这通常适用于您使用unicode_modifiable的局部变量,并且可能是当在同一分配中没有第二个运算符时,编译器设法生成此类行为。