为什么在循环中重复添加字符串时,f字符串比字符串连接慢?



我正在对一个带有timeit的项目的一些代码进行基准测试(使用免费的replit,所以1024MB内存):

code = '{"type":"body","layers":['
for x, row in enumerate(pixels):
for y, pixel in enumerate(row):
if pixel != (0, 0, 0, 0):
code += f'''{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{'#%02x%02x%02x' % (pixel[:3])}","hideBorder":1}},'''

code += '],"sides":1,"name":"Image"}}

循环对给定图像中的每个像素运行(当然不是很有效,但我还没有实现任何减少循环时间的东西),所以我可以在循环中得到的任何优化都是值得的。

我记得只要组合3个+字符串,f-string就比字符串连接快——如图所示,我有很多超过3个字符串被组合——所以我决定用f-string替换循环中的+=,看看效果如何。

code = '{"type":"body","layers":['
for x, row in enumerate(pixels):
for y, pixel in enumerate(row):
if pixel != (0, 0, 0, 0):
code = f'''{code}{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{'#%02x%02x%02x' % (pixel[:3])}","hideBorder":1}},'''

code += '],"sides":1,"name":"Image"}}

500次timeit迭代的结果:

+= took 5.399778672000139 seconds
fstr took 6.91279206800027 seconds

我已经运行了很多次了;上述时间是迄今为止f-string完成的最佳时间。为什么f字符串在这种情况下更慢?

PS:这是我第一次在这里发问题。如有任何建议能帮助我改进今后的问题,我将不胜感激。D

因此,首先,在具有不可变字符串的语言中,重复连接在理论上是O(n²),而有效实现的批量连接是O(n),因此两个版本的代码在理论上都不适合重复连接。在O(n)工作的任何地方工作的版本是:

code = ['{"type":"body","layers":[']  # Use list of str, not str
for x, row in enumerate(pixels):
for y, pixel in enumerate(row):
if pixel != (0, 0, 0, 0):
code.append(f'''{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{'#%02x%02x%02x' % (pixel[:3])}","hideBorder":1}},''')  # Append each new string to list

code.append('],"sides":1,"name":"Image"}}')
code = ''.join(code)  # Efficiently join list of str back to single str

你的+=代码碰巧足够有效地工作,因为当连接到一个没有其他活引用的字符串时,CPython对字符串连接进行了特定的优化,但是PEP8风格指南中的第一个编程建议特别警告不要依赖它:

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

本质上,原始的基于+=的代码受益于优化,因此,最终执行的数据副本更少。你基于f字符串的代码做了同样的工作,但以一种阻止CPython优化应用的方式(每次构建一个全新的,越来越大的str)。这两种方法都是糟糕的形式,其中一种在CPython上稍微不那么糟糕。当您的热代码执行重复连接时,您已经做错了,只需在末尾使用str''.joinlist

答案在评论和提供的链接中。

您会发现这个实现(原始示例的)执行得更好:

img = Image.open(r'F:ProjectPythonsandbox_310test.png')
pixels = list(img.getdata())
width, height = img.size
pixels = tuple(pixels[i * width:(i + 1) * width] for i in range(height))
start = 6
gap, size = (start * 2) / (width - 1), 0.1475 * (64 / width) * (start / 6)
data = [(-start + x * gap, start - y * gap, '#%02x%02x%02x' % (pixel[:3]))
for x, row in enumerate(pixels) for y, pixel in enumerate(row)]
template = f'''{{{{"offsetX":{{}},"offsetY":{{}},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{{}}","hideBorder":1}}}},'''
code = '{"type":"body","layers":[' + ''.join([template.format(*t) for t in data]) + '],"sides":1,"name":"Image"}}'

编辑:用户@kellybundy问有多快:

from PIL import Image
from timeit import timeit
img = Image.open(r'F:ProjectPythonsandbox_310test.png')
pixels = list(img.getdata())
width, height = img.size
pixels = tuple(pixels[i * width:(i + 1) * width] for i in range(height))
start = 6
gap, size = (start * 2) / (width - 1), 0.1475 * (64 / width) * (start / 6)

def f_sol():
data = [(-start + x * gap, start - y * gap, '#%02x%02x%02x' % (pixel[:3]))
for x, row in enumerate(pixels) for y, pixel in enumerate(row)]
template = f'''{{{{"offsetX":{{}},"offsetY":{{}},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{{}}","hideBorder":1}}}},'''
code = '{"type":"body","layers":[' + ''.join([template.format(*t) for t in data]) + '],"sides":1,"name":"Image"}}'
return code

def f_op():
code = '{"type":"body","layers":['
for x, row in enumerate(pixels):
for y, pixel in enumerate(row):
if pixel != (0, 0, 0, 0):
code += f'''{{"offsetX":{-start + x * gap},"offsetY":{start - y * gap},"rot":45,"size":{size},"sides":4,"outerSides":0,"outerSize":0,"team":"{'#%02x%02x%02x' % (pixel[:3])}","hideBorder":1}},'''
code += '],"sides":1,"name":"Image"}}'
return code

assert f_sol() == f_op()
print(timeit(f_sol, number=10))
print(timeit(f_op, number=10))

输出:

1.7875813000027847
47.82409440000265

所以,超过25倍的速度,这就是为什么我没有开始计时。

相关内容

  • 没有找到相关文章

最新更新