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

gun*_*gor 8 python string cpython string-concatenation python-internals

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

new_file = new_file + line + string
Run Code Online (Sandbox Code Playgroud)

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

new_file += line + string
Run Code Online (Sandbox Code Playgroud)

这是代码:

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))
Run Code Online (Sandbox Code Playgroud)

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

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

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

Sha*_*ger 25

CPython(引用解释器)具有就地字符串连接的优化(当附加到的字符串没有其他引用时).这样做时,它不能作为可靠地应用这种优化的+,只有+=(+包括两个活动的引用,分配对象和操作数,前者不参与+经营,所以它很难去优化它).

根据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"
Run Code Online (Sandbox Code Playgroud)

与以下内容完全不同:

new_file_content = new_file_content + line.strip() + "~" + row_region + "\n"
Run Code Online (Sandbox Code Playgroud)

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

new_file_content = (((new_file_content + line.strip()) + "~") + row_region) + "\n"
Run Code Online (Sandbox Code Playgroud)

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

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

new_file_content = new_file_content + (line.strip() + "~" + row_region + "\n")
Run Code Online (Sandbox Code Playgroud)

或稍慢,但仍然比慢速代码快很多倍,因为它保持了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"
Run Code Online (Sandbox Code Playgroud)

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

当然,根据PEP8指南,即使+=在这里使用也是不好的形式.在大多数使用不可变字符串的语言中(包括大多数非CPython Python解释器),重复的字符串连接是Schlemiel the Painter算法的一种形式,它会导致严重的性能问题.正确的解决方案是构建一个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)
Run Code Online (Sandbox Code Playgroud)

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

附注:对于您的具体情况,您根本不应该累积或连接.你有一个输入文件和一个输出文件,可以逐行处理.每次你要追加或累积文件内容时,只需将它们写出来(我已经清理了一些PEP8兼容性以及其他小样式改进的代码):

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)
Run Code Online (Sandbox Code Playgroud)

实施细节深入探讨

对于那些对实现细节感兴趣的人,CPython字符串concat优化是在字节码解释器中实现的,而不是在str类型本身上实现(从技术上讲,PyUnicode_Append是否进行了变异优化,但它需要解释器的帮助来修复引用计数,因此它知道它可以安全地使用优化;没有解释器帮助,只有C扩展模块才能从优化中受益).

当解释器检测到两个操作数都是Python级别str类型时(在C层,在Python 3中,它仍然被称为PyUnicode,2.x天的遗产,不值得更改),它调用一个特殊unicode_concatenate函数,它检查下一条指令是否是三条基本STORE_*指令之一.如果是,并且目标与左操作数相同,则清除目标引用,因此PyUnicode_Append将只看到对操作数的单个引用,允许它为str具有单个引用的a调用优化代码.

这意味着您不仅可以通过执行来打破优化

a = a + b + c
Run Code Online (Sandbox Code Playgroud)

如果有问题的变量不是顶级(全局,嵌套或本地)名称,您也可以将其分解.如果你正在操作一个对象属性,一个list索引,一个dict值等,甚至+=不会帮助你,它就不会看到"简单STORE",所以它不会清除目标引用,所有这些获得超低的,非就地行为:

foo.x += mystr
foo[0] += mystr
foo['x'] += mystr
Run Code Online (Sandbox Code Playgroud)

它也是特定的str类型; 在Python 2中,优化对unicode对象没有帮助,而在Python 3中,它对bytes对象没有帮助,并且在两个版本中它都不会针对子类进行优化str; 那些总是走慢路.

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


jsb*_*eno 7

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

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

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

当人们对语言的工作方式有所了解时,从小字符串构建大型文本体的方法是创建一个包含所有字符串的Python列表,并在循环结束后,对str.join所有传入的方法进行一次调用字符串组件.即使在Python实现中也是如此,并且不依赖于能够被触发的优化.

output = []
for ...:
    output.append(line)

new_file = "\n".join(output)
Run Code Online (Sandbox Code Playgroud)


归档时间:

查看次数:

1108 次

最近记录:

8 年,9 月 前