为什么'new_file + = line + string'比'new_file = new_file + line + string'快得多?

时间:2016-12-06 13:41:49

标签: python string string-concatenation cpython python-internals

当我们使用时,我们的代码需要10分钟来虹吸68,000条记录:

new_file = new_file + line + string

但是,当我们执行以下操作时,只需1秒钟:

new_file += line + string

以下是代码:

for line in content:
import time
import cmdbre

fname = "STAGE050.csv"
regions = cmdbre.regions
start_time = time.time()
with open(fname) as f:
        content = f.readlines()
        new_file_content = ""
        new_file = open("CMDB_STAGE060.csv", "w")
        row_region = ""
        i = 0
        for line in content:
                if (i==0):
                        new_file_content = line.strip() + "~region" + "\n"
                else:
                        country = line.split("~")[13]
                        try:
                                row_region = regions[country]
                        except KeyError:
                                row_region = "Undetermined"
                        new_file_content += line.strip() + "~" + row_region + "\n"
                print (row_region)
                i = i + 1
        new_file.write(new_file_content)
        new_file.close()
        end_time = time.time()
        print("total time: " + str(end_time - start_time))

我在python中编写的所有代码都使用第一个选项。这只是基本的字符串操作......我们正在从文件读取输入,处理它并将其输出到新文件。我100%肯定第一种方法比第二种方法运行时间大约长600倍,但为什么呢?

正在处理的文件是csv,但使用〜而不是逗号。我们在这里所做的就是使用这个csv,它有一个国家列,并为国家/地区添加一列,例如LAC,EMEA,NA等... cmdbre.regions只是一本字典,以所有约200个国家为关键,每个区域为价值。

一旦我改为追加字符串操作......循环在1秒而不是10分钟内完成... csv中的68,000条记录。

2 个答案:

答案 0 :(得分:25)

CPython(引用解释器)具有就地字符串连接的优化(当附加到的字符串没有其他引用时)。在执行+时,它无法可靠地应用此优化,只有+=+涉及两个实时引用,分配目标和操作数,前者不是{&1;}。参与+操作,因此更难以优化它。)

根据PEP 8

,您不应该依赖此
  

代码的编写应该不会影响Python的其他实现(PyPy,Jython,IronPython,Cython,Psyco等)。

     

例如,不要依赖CPython对a + = b或a = a + b形式的语句的就地字符串连接的有效实现。这种优化即使在CPython中也很脆弱(它仅适用于某些类型),并且在不使用引用计数的实现中根本不存在。在库的性能敏感部分中,应使用' .join()表单。这将确保连接在各种实现中以线性时间发生。

根据问题修改进行更新:是的,您打破了优化。你连接了许多字符串,而不仅仅是一个字符串,Python从左到右进行评估,所以它必须先进行最左边的连接。因此:

new_file_content += line.strip() + "~" + row_region + "\n"

与以下内容完全不同:

new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"

因为前者将所有 new 块连接在一起,然后将它们一次性地附加到累加器字符串,而后者必须从左到右评估每个添加的临时值。涉及new_file_content本身。为了清晰起见,添加了parens,就像你做的那样:

new_file_content = (((new_file_content + line.strip()) + "~") + row_region) + "\n"

因为在到达它们之前它实际上并不知道类型,所以它不能假设所有这些都是字符串,因此优化不会起作用。

如果您将第二位代码更改为:

new_file_content = new_file_content + (line.strip() + "~" + row_region + "\n")

或稍慢,但仍然比慢速代码快很多倍,因为它保持了CPython优化:

new_file_content = new_file_content + line.strip()
new_file_content = new_file_content + "~"
new_file_content = new_file_content + row_region
new_file_content = new_file_content + "\n"

所以CPython的积累是显而易见的,你可以解决性能问题。但坦率地说,只要您执行像这样的逻辑追加操作,就应该使用+=; +=存在是有原因的,它为维护者和解释者提供了有用的信息。除此之外,就DRY而言,这是一种很好的做法;为什么在不需要时将变量命名为?

当然,根据PEP8指南,即使在这里使用+=也是不好的形式。在大多数具有不可变字符串的语言中(包括大多数非CPython Python解释器),重复的字符串连接是Schlemiel the Painter's Algorithm的一种形式,这会导致严重的性能问题。正确的解决方案是构建list个字符串,然后join将它们全部组合在一起,例如:

    new_file_content = []
    for i, line in enumerate(content):
        if i==0:
            # In local tests, += anonymoustuple runs faster than
            # concatenating short strings and then calling append
            # Python caches small tuples, so creating them is cheap,
            # and using syntax over function calls is also optimized more heavily
            new_file_content += (line.strip(), "~region\n")
        else:
            country = line.split("~")[13]
            try:
                    row_region = regions[country]
            except KeyError:
                    row_region = "Undetermined"
            new_file_content += (line.strip(), "~", row_region, "\n")

    # Finished accumulating, make final string all at once
    new_file_content = "".join(new_file_content)

即使CPython字符串连接选项可用也通常更快,并且在非CPython Python解释器上也可以快速运行,因为它使用可变list来有效地累积结果,然后允许{{1预计算字符串的总长度,一次性分配最终字符串(而不是沿途增量调整大小),并将其填充一次。

旁注:根据您的具体情况,您根本不应该累积或连接。您已获得输入文件和输出文件,并且可以逐行处理。每次你要追加或累积文件内容时,只需将它们写出来(我已经清理了一些代码,以便在进行PEP8时遵循PEP8和其他小的风格改进):

''.join

实施细节深入研究

对于对实现细节感兴趣的人,CPython字符串concat优化是在字节码解释器中实现的,而不是start_time = time.monotonic() # You're on Py3, monotonic is more reliable for timing # Use with statements for both input and output files with open(fname) as f, open("CMDB_STAGE060.csv", "w") as new_file: # Iterate input file directly; readlines just means higher peak memory use # Maintaining your own counter is silly when enumerate exists for i, line in enumerate(f): if not i: # Write to file directly, don't store new_file.write(line.strip() + "~region\n") else: country = line.split("~")[13] # .get exists to avoid try/except when you have a simple, constant default row_region = regions.get(country, "Undetermined") # Write to file directly, don't store new_file.write(line.strip() + "~" + row_region + "\n") end_time = time.monotonic() # Print will stringify arguments and separate by spaces for you print("total time:", end_time - start_time) 类型本身(技术上,str进行了突变优化,但它需要帮助从解释器修复引用计数,因此它知道它可以安全地使用优化;没有解释器帮助,只有C扩展模块才能从优化中受益)。

当翻译detects that both operands are the Python level str type(在C层,在Python 3中,它仍然被称为PyUnicode_Append时,2.x天的遗产不值得改变),它调用a special unicode_concatenate function,它检查下一条指令是否是三条基本PyUnicode指令之一。如果是,并且目标与左操作数相同,则清除目标引用,因此STORE_*将只看到对操作数的单个引用,允许它调用PyUnicode_Append的优化代码只有一个参考。

这意味着您不仅可以通过

来打破优化
str

如果有问题的变量不是顶级(全局,嵌套或本地)名称,您也可以将其分解。如果您正在对某个对象属性,a = a + b + c 索引,list值等进行操作,即使dict也无法帮助您,它也会赢得'看到"简单+=",所以它没有清除目标参考,所有这些都得到超低的非就地行为:

STORE

它也特定于foo.x += mystr foo[0] += mystr foo['x'] += mystr 类型;在Python 2中,优化对str个对象没有帮助,而在Python 3中,它对unicode个对象没有帮助,并且在两个版本中它都不会优化子类的bytes;那些总是走慢路。

基本上,对于刚接触Python的人来说,最简单的常见情况下,优化是尽可能好的,但即使是中等复杂的情况也不会给它带来严重的麻烦。这只是强调了PEP8的建议:通过做正确的事情并使用{{1},当你可以在每个解释器,任何商店目标上运行得更快时,根据你的解释器的实现细节是个坏主意。 }。

答案 1 :(得分:7)

实际上,两者都可能同样慢,但对于某些优化而言,这实际上是官方Python运行时(cPython)的实现细节。

Python中的字符串是不可变的 - 这意味着当你执行“str1 + str2”时,Python必须创建第三个字符串对象,并将所有内容从str1和str2复制到它 - 无论任何大小这些部分是。

inplace运算符允许Python使用一些内部优化,以便str1中的所有数据不必再次复制 - 甚至可能允许一些缓冲区空间用于进一步的连接选项。

当人们对语言的工作方式有所了解时,从小字符串构建大型文本体的方法是创建一个包含所有字符串的Python列表,在循环结束后,只需调用{传递所有字符串组件的{1}}方法。即使在Python实现中,这也将一直很快,并且不依赖于能够被触发的优化。

str.join