Why is 'new_file += line + string' so much faster than 'new_file = new_file + line + string'?
当我们使用以下代码时,我们的代码需要10分钟来虹吸68000条记录:
1 | new_file = new_file + line + string |
但是,当我们执行以下操作时,只需1秒钟:
1 | new_file += line + string |
号
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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" +" " else: country = line.split("~")[13] try: row_region = regions[country] except KeyError: row_region ="Undetermined" new_file_content += line.strip() +"~" + row_region +" " 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编写的所有代码都使用第一个选项。这只是基本的字符串操作…我们从一个文件中读取输入,对其进行处理并将其输出到新文件中。我百分之百肯定第一种方法的运行时间大约是第二种方法的600倍,但为什么呢?
正在处理的文件是csv,但使用~而不是逗号。我们在这里所做的就是取这个csv,它有一个国家列,并为国家区域添加一个列,例如lac、emea、na等…cmdbre.regions只是一个字典,以大约200个国家为键,每个地区为值。
一旦我改为附加字符串操作…循环在1秒而不是10分钟内完成…csv中有68000条记录。
cpython(引用解释器)对就地字符串连接进行了优化(当附加到的字符串没有其他引用时)。在执行
但是,根据PEP 8,你不应该依赖于此:好的。
Code should be written in a way that does not disadvantage other implementations of Python (PyPy, Jython, IronPython, Cython, Psyco, and such).
Ok.
For example, do not rely on CPython's efficient implementation of in-place string concatenation for statements in the form a += b or a = a + b . This optimization is fragile even in CPython (it only works for some types) and isn't present at all in implementations that don't use refcounting. In performance sensitive parts of the library, the ''.join() form should be used instead. This will ensure that concatenation occurs in linear time across various implementations.
Ok.
号
基于问题编辑的更新:是的,你打破了优化。您连接了多个字符串,而不仅仅是一个,而且python的计算结果是从左到右的,所以它必须首先进行最左边的连接。因此:好的。
1 2 | new_file_content += line.strip() +"~" + row_region +" " |
完全不同于:好的。
1 2 | new_file_content = new_file_content + line.strip() +"~" + row_region +" " |
号
因为前者将所有新的部分连接在一起,然后将它们全部附加到累加器字符串中,而后者必须使用不涉及
1 2 | new_file_content = (((new_file_content + line.strip()) +"~") + row_region) +" " |
因为在到达类型之前它实际上并不知道这些类型,所以它不能假定所有这些类型都是字符串,所以优化不会开始。好的。
如果您将代码的第二位更改为:好的。
1 2 | new_file_content = new_file_content + (line.strip() +"~" + row_region +" ") |
。
或者稍微慢一点,但仍然比慢代码快很多倍,因为它保持了cpython优化:好的。
1 2 3 4 5 | 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 +" " |
所以积累对cpython来说是显而易见的,你可以解决性能问题。但坦率地说,在执行这样的逻辑追加操作时,应该只使用
当然,根据PEP8的指导方针,即使使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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 ") else: country = line.split("~")[13] try: row_region = regions[country] except KeyError: row_region ="Undetermined" new_file_content += (line.strip(),"~", row_region," ") # Finished accumulating, make final string all at once new_file_content ="".join(new_file_content) |
它通常更快,甚至当cpython字符串连接选项可用时,而且在非cpython python解释器上也会可靠地更快,因为它使用可变的
旁注:对于您的特定情况,您根本不应该累积或连接。您有一个输入文件和一个输出文件,可以逐行处理。每次您要附加或积累文件内容时,只需将它们写出来(我已经清理了一点代码,以满足PEP8的遵从性和其他一些小的样式改进,而我正在进行这些改进):好的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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 ") 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 +" ") end_time = time.monotonic() # Print will stringify arguments and separate by spaces for you print("total time:", end_time - start_time) |
号实施细节深入研究
对于那些对实现细节感兴趣的人,cpython字符串concat优化是在字节码解释器中实现的,而不是在
当解释器检测到两个操作数都是python级别的
这意味着不仅可以通过执行好的。
1 | a = a + b + c |
您也可以在所讨论的变量不是顶级(全局、嵌套或本地)名称时中断它。如果你在操作一个对象属性、一个
1 2 3 | foo.x += mystr foo[0] += mystr foo['x'] += mystr |
。
它还针对
基本上,对于刚接触Python的人来说,在最简单的常见情况下,优化是尽可能好的,但是对于更为复杂的情况,优化不会带来严重的麻烦。这就加强了PEP8的建议:根据您的解释器的实现细节,当您可以通过正确的操作和使用
实际上,这两个过程都可能同样缓慢,但对于某些优化来说,这实际上是官方Python运行时(cpython)上的一个实现细节。
python中的字符串是不可变的——这意味着当您执行"str1+str2"操作时,python必须创建第三个字符串对象,并将str1和str2中的所有内容复制到它上面——不管这些部分有多大。
inplace操作符允许python使用一些内部优化,这样str1中的所有数据就不必再次复制,甚至可能还允许一些缓冲区空间用于进一步的连接选项。
当我们了解到语言的工作方式时,用小字符串构建大文本体的方法是用所有字符串创建一个python列表,循环结束后,对传入所有字符串组件的
1 2 3 4 5 6 | output = [] for ...: output.append(line) new_file =" ".join(output) |