【问题标题】:Why does the order of string concatenation in python affect speed greatly?为什么python中字符串连接的顺序对速度影响很大?
【发布时间】:2018-11-19 13:22:28
【问题描述】:

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

方法一:

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

这非常慢 - 在大约第 100,000 条消息之后,添加每条消息大约需要 2-3 秒,而在第 300,000 条消息左右,这个过程基本停止了。

方法二:

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

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

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

【问题讨论】:

  • 奖励:total_str += m + '\n'也比方法 #1 快。
  • 您的实现正遭受与在这样的 for 循环中连接不可变字符串相关的性能问题;行为是多项式时间。但是,我怀疑 CPython 运行时中的窥孔优化器能够使用临时变量优化版本。这是一个不应该依赖的实现细节。使用messages.append("") 然后'\n'.join(messages)
  • @juanpa.arrivillaga 我不认为这是一种优化。我认为恰恰相反。 CPython 愚蠢到将a + b + c 执行为(a + b) + c,这导致长字符串a 被复制两次。 (而a + (b + c) 只会复制一次)

标签: python string performance


【解决方案1】:

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

我猜为什么第二个实现会快得多(不仅仅是快两倍),是因为有一些优化,有时可以允许 CPython 根本不执行左操作数的副本。 PyUnicode_Append 似乎正好有这样的优化,基于 unicode_modifiable,如果引用计数正好为 1,字符串从未被散列,以及其他一些条件,它可以改变对象。这通常适用于您使用+= 的局部变量,并且可能编译器在同一赋值中没有第二个运算符时设法生成这种行为。

【讨论】:

    【解决方案2】:

    a + b + c 不是将a、b 和c 连接成单个字符串的单个操作。是两次操作,t = a + b和t + c,意思是复制a的内容两次;一次将a 复制到t,然后当t 被复制到t + c 的结果中。由于在您的示例中,a 是不断变长的字符串,因此您最好将每一步复制的数据量加倍。

    最好的办法是避开+创建的所有临时str对象,使用join:

    total_str = "\n".join(messages)
    

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

    【讨论】:

    • 很好的答案,只是a + b 必须创建a 的副本不是很明显。您可能需要更详细地解释该部分。
    【解决方案3】:

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

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

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

    【讨论】:

      猜你喜欢
      • 2015-01-10
      • 2018-03-09
      • 2013-10-05
      • 1970-01-01
      • 2013-11-19
      • 2011-03-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多