When I run this program on Python 3.12.6, debian trixie:
# Part 1
def test():
s = 'a'*1_000_000
t = ''
for c in s:
print(len(t), id(t))
t += c
test()
##############################################
# Part 2
s = 'a'*1_000_000
t = ''
for c in s:
print(len(t), id(t))
t += c
part 1 is much faster than part 2. It seems that in part 2, Python copies the string at each step.
What is the reason for this phenomenon?
My guess is that its the cost of access to local variables, part 1, compared to the cost of access to global variables in part 2.
In both cases the string manipulation of t += c is the same.
Note that each time you call t += c python actually does t = t + c that creates a new string to replace t every time around the loop.
CPython actually does specific optimizations for string concatenation that means it tries to reuse the original object and avoid copying. I suspect this doesn’t always work, but even if it only works sometimes it can be a huge difference.
This can only work if the reference count of the object is exactly 1. I suspect that with the changed behavior of global variable accesses compared function locals this optimization is also prevented, in addition to what @barry-scott already mentioned of global variables being slower to access than function locals.
It is not related to strings, but rather to the speed of accessing a local variable versus a global variable.
import time
def bench():
a = 0
for i in range(1000000):
a += i
return a
start = time.time()
result = bench()
print(time.time() - start)
print(result)
start = time.time()
a = 0
for i in range(1000000):
a += i
print(time.time() - start)
print(result)
@elis.byberi Did did you have a look at the GitHub issue with the explanation? And why did you measure ints instead of strings? That doesn’t show the huge difference.
0.03 seconds for part 1 versus 15 seconds for part 2.
The prints highlight the fact that the string t is copied at each step in part 2, while this is not the case in part 1.
The link given by Stephan answers my question, thanks !
import time
def bench_local():
s = 'a' * 100_000
t = ''
for c in s:
t += c
return len(t)
start = time.time()
result = bench_local()
print(time.time() - start)
print(result)
s = 'a' * 100_000
t = ''
def bench_global():
global s
global t
for c in s:
t += c
return len(t)
start = time.time()
result = bench_global()
print(time.time() - start)
print(result)
import time
def bench_string():
s = 'a' * 1000000
t = ''
for c in s:
t += c
return len(t)
start = time.time()
result = bench_string()
print(time.time() - start)
print(result)
def bench_integer():
a = 0
for i in range(1000000):
a += i
return a
start = time.time()
result = bench_integer()
print(time.time() - start)
print(result)
That doesn’t seem to cause a significant slowdown. In Python 3.10, there are no copies, but the slowdown remains the same:
# Part 1
def test():
s = 'a' * 1_000_000
t = 'b' * 1_000_000
for c in s[:10]:
print(len(t), id(t))
t += c
test()
##############################################
# Part 2
s = 'a' * 1_000_000
t = 'b' * 1_000_000
for c in s[:10]:
print(len(t), id(t))
t += c
import time
s = 'a'*1_000_000
def testo():
t = ''
def testi():
start = time.time()
nonlocal t
for c in s:
t += c
print(str(time.time() - start))
testi()
testo()