Skip to content

free-threading one line comprehension is significantly slower than multi line #159493

Open

Description

@johng

Bug report

import sys
import time
from concurrent.futures import ThreadPoolExecutor

if sys._is_gil_enabled():
    sys.exit("the GIL is enabled: run this with a free-threaded build (python3.Xt)")

# slow
def tag_fused(vocab, batch):
    return [vocab.get(tok, "unk") for tok in batch]

# fast
def tag_unfused(vocab, batch):
    return [vocab.get(tok, "unk")
            for tok in batch]


def seconds(tag, vocab, batch, nthreads, nbatches=600):
    with ThreadPoolExecutor(nthreads) as pool:
        t0 = time.perf_counter()
        for a in pool.map(tag, [vocab] * nbatches, [batch] * nbatches):
            pass
        return time.perf_counter() - t0


def main():
    tokens = [sys.intern(f"tok{i}") for i in range(20000)] * 10
    vocab = {tok: ("noun", "verb", "adj")[i % 3] for i, tok in enumerate(tokens)}
    for tag in (tag_fused, tag_unfused):
        t1 = seconds(tag, vocab, tokens, 1)
        t6 = seconds(tag, vocab, tokens, 6)
        print(f"{tag.__name__:11s} 1 thread {t1:.3f}s   6 threads {t6:.3f}s   "
              f"speedup {t1 / t6:.1f}x")


main()

python3.15t Tools/ftlockbench/repro_store_fast_load_fast_borrow.py
tag_fused 1 thread 2.624s 6 threads 12.444s speedup 0.2x
tag_unfused 1 thread 2.549s 6 threads 0.596s speedup 4.3x

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)

CPython versions tested on:

CPython main branch

Operating systems tested on:

macOS

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    on Oct 15, 2026
  2. picnixz commented on Oct 15, 2026

    @picnixz
    Member

    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)])

  3. eendebakpt commented on Oct 16, 2026

    @eendebakpt
    Contributor

    A smaller reproducer:

    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

  4. eendebakpt commented on Oct 16, 2026

    @eendebakpt
    Contributor

    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.

    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, 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.

  5. added
    type-featureA feature request or enhancement
    and removed
    type-bugAn unexpected behavior, bug, or error
    on Oct 16, 2026
  6. picnixz commented on Oct 16, 2026

    @picnixz
    Member

    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)

  7. johng commented on Oct 17, 2026

    @johng
    ContributorAuthor

    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?

  8. kumaraditya303 commented on Oct 21, 2026

    @kumaraditya303
    Contributor

    I think this is a compiler bug somewhere, I'll work on fixing this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions