You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
free-threading one line comprehension is significantly slower than multi line #159493
With identical functions apart from the multi line list comprehension that does a load and store specialisation the multi line breaks the optimisations to STORE_FAST_LOAD_FAST resulting in a severe slowdown in free threading python.
If we disassemble the unfused case no
for tag in (tag_fused, tag_unfused):
load = next(i for i in dis.get_instructions(tag) if "vocab" in i.argrepr)
print(f"{tag.__name__:11s} loads vocab with {load.opname} ({load.argrepr})")
This is due to the different emited op codes.
tag_fused loads vocab with STORE_FAST_LOAD_FAST (tok, vocab)
tag_unfused loads vocab with LOAD_FAST_BORROW (vocab)
That's... surprsing. The opcodes are also different even on default build (at least on Python 3.14.7 (main, Aug 5 2026, 10:29:49) [Clang 21.0.0 (clang-2100.1.1.101)])
def fused(shared, n):
for i in range(n): shared # STORE_FAST_LOAD_FAST (i, shared)
def unfused(shared, n):
for i in range(n):
shared # STORE_FAST i; LOAD_FAST_BORROW shared
A potential solution is to make the STORE_FAST_LOAD_FAST have a borrowed version as well. We can also check the opcode statistics (an older set of statistics bm-20250719-3.15.0a0-800d37f shows the STORE_FAST_LOAD_FAST is not very common) whether we still need the STORE_FAST_LOAD_FAST.
The reason the two cases are handled differently is in make_super_instruction from in #105230. @markshannon
In all cases where STORE_FAST_LOAD_FAST is used we can use a STORE_FAST_LOAD_FAST_BORROW. To not increase the number of opcodes we can therefore replace STORE_FAST_LOAD_FAST with STORE_FAST_LOAD_FAST_BORROW. Allowing super instructions over multiple lines does not seem to happen often and making that work might lead to some tricky situations, so am leaving that idea for now.
All code objects from compiling the 2042 files of Lib/ (counts of instructions, CACHE entries excluded):
Opcode
main
this PR
STORE_FAST_LOAD_FAST
1,906
0
STORE_FAST_LOAD_FAST_BORROW
0
1,728
LOAD_FAST_BORROW_LOAD_FAST_BORROW
40,813
41,002
LOAD_FAST (strong)
19,696
19,761
LOAD_FAST_BORROW
509,464
509,199
all instructions
4,825,091
4,825,080
The number of STORE_FAST_LOAD_FAST/STORE_FAST_LOAD_FAST_BORROW is not identical because the optimization is at different place, so other super instructions might get priority. Nevertheless, the bulk of the STORE_FAST_LOAD_FAST is converted to the borrow type. This resolves the issue here (refcount contention), but also improve single-threaded performance
Benchmark
main
prototype branch
[vocab.get(tok, "unk") for tok in batch] (one line)
90.2 us
85.6 us: 1.05x faster
same comprehension split over two lines
84.6 us
84.3 us: same
for x in it: a.append(x) (one line)
65.6 us
60.8 us: 1.08x faster
same loop with the body on its own line
60.9 us
59.4 us: same
for x in it: n = n + 1
50.8 us
49.7 us: 1.02x faster
[i*2 for i in ints], 10,000 ints
635 us
610 us: 1.04x faster
[2*i for i in ints] (no store/load pair, control)
619 us
625 us: same
Note: removing the bug label. The code works (slower, but still works), so no backports are needed imo.
I still think it's a bug somehow because newlines should not result in a change of bytecode IMO. And the fact that it's significantly slower is annoying. But we don't need to backport it (but I still think it's a bug)
Agree with the above that newlines or anything which doesn't change the actual representation of the program should not impact the bytecode. Should the fix focus on why there is a divergence there?
Activity
picnixz commentedon Oct 15, 2026
That's... surprsing. The opcodes are also different even on default build (at least on
Python 3.14.7 (main, Aug 5 2026, 10:29:49) [Clang 21.0.0 (clang-2100.1.1.101)])eendebakpt commentedon Oct 16, 2026
A smaller reproducer:
A potential solution is to make the
STORE_FAST_LOAD_FASThave a borrowed version as well. We can also check the opcode statistics (an older set of statistics bm-20250719-3.15.0a0-800d37f shows theSTORE_FAST_LOAD_FASTis not very common) whether we still need theSTORE_FAST_LOAD_FAST.The reason the two cases are handled differently is in
make_super_instructionfrom in #105230. @markshannoneendebakpt commentedon Oct 16, 2026
In all cases where
STORE_FAST_LOAD_FASTis used we can use aSTORE_FAST_LOAD_FAST_BORROW. To not increase the number of opcodes we can therefore replaceSTORE_FAST_LOAD_FASTwithSTORE_FAST_LOAD_FAST_BORROW. Allowing super instructions over multiple lines does not seem to happen often and making that work might lead to some tricky situations, so am leaving that idea for now.A prototype PR is: main...eendebakpt:cpython:store-fast-load-fast-borrow
All code objects from compiling the 2042 files of
Lib/(counts of instructions,CACHEentries excluded):STORE_FAST_LOAD_FASTSTORE_FAST_LOAD_FAST_BORROWLOAD_FAST_BORROW_LOAD_FAST_BORROWLOAD_FAST(strong)LOAD_FAST_BORROWThe number of
STORE_FAST_LOAD_FAST/STORE_FAST_LOAD_FAST_BORROWis not identical because the optimization is at different place, so other super instructions might get priority. Nevertheless, the bulk of theSTORE_FAST_LOAD_FASTis converted to the borrow type. This resolves the issue here (refcount contention), but also improve single-threaded performance[vocab.get(tok, "unk") for tok in batch](one line)for x in it: a.append(x)(one line)for x in it: n = n + 1[i*2 for i in ints], 10,000 ints[2*i for i in ints](no store/load pair, control)Note: removing the bug label. The code works (slower, but still works), so no backports are needed imo.
picnixz commentedon Oct 16, 2026
I still think it's a bug somehow because newlines should not result in a change of bytecode IMO. And the fact that it's significantly slower is annoying. But we don't need to backport it (but I still think it's a bug)
johng commentedon Oct 17, 2026
Agree with the above that newlines or anything which doesn't change the actual representation of the program should not impact the bytecode. Should the fix focus on why there is a divergence there?
kumaraditya303 commentedon Oct 21, 2026
I think this is a compiler bug somewhere, I'll work on fixing this.