Skip to content

Performance regression for making bound methods #83298

Description

@rhettinger
BPO 39117
Nosy @rhettinger, @vstinner, @benjaminp, @methane, @serhiy-storchaka, @pablogsal

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2019-12-21.20:13:01.396>
labels = ['interpreter-core', '3.9', 'performance']
title = 'Performance regression for making bound methods'
updated_at = <Date 2020-04-28.21:51:46.108>
user = 'https://github.com/rhettinger'

bugs.python.org fields:

activity = <Date 2020-04-28.21:51:46.108>
actor = 'vstinner'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Interpreter Core']
creation = <Date 2019-12-21.20:13:01.396>
creator = 'rhettinger'
dependencies = []
files = []
hgrepos = []
issue_num = 39117
keywords = []
message_count = 7.0
messages = ['358781', '358802', '358807', '367556', '367566', '367567', '367568']
nosy_count = 6.0
nosy_names = ['rhettinger', 'vstinner', 'benjamin.peterson', 'methane', 'serhiy.storchaka', 'pablogsal']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'performance'
url = 'https://bugs.python.org/issue39117'
versions = ['Python 3.9']

Activity

  1. rhettinger commented on Dec 21, 2019

    @rhettinger
    ContributorAuthor
    $ python3.9 -m timeit -r 11 -s 'class A: pass' -s 'A.m = lambda s: None' -s 'a = A()' 'a.m; a.m; a.m; a.m; a.m'
    1000000 loops, best of 11: 230 nsec per loop
    
    $ python3.8 -m timeit -r 11 -s 'class A: pass' -s 'A.m = lambda s: None' -s 'a = A()' 'a.m; a.m; a.m; a.m; a.m'
    2000000 loops, best of 11: 149 nsec per loop
    
    $ python3.7 -m timeit -r 11 -s 'class A: pass' -s 'A.m = lambda s: None' -s 'a = A()' 'a.m; a.m; a.m; a.m; a.m'
    2000000 loops, best of 11: 159 nsec per loop
    
    $ python3.6 -m timeit -r 11 -s 'class A: pass' -s 'A.m = lambda s: None' -s 'a = A()' 'a.m; a.m; a.m; a.m; a.m'
    10000000 loops, best of 11: 0.159 usec per loop

    Timings made using the recent released python.org macOS 64-bit builds.

  2. benjaminp commented on Dec 23, 2019

    @benjaminp
    Contributor

    Probably bpo-37340

  3. methane commented on Dec 23, 2019

    @methane
    Member

    Is this regression is large enough to revive the free_list for bound methods?

  4. rhettinger commented on Apr 28, 2020

    @rhettinger
    ContributorAuthor

    This performance regression in still present in 3.9.0a6

    Results from Tools/scripts/var_access_benchmark.py:

                         Python 3.8.2       Python 3.9.0a6
                         ------------       \--------------
    

    read_boundmethod 27.7 ns 47.1 ns

    Is this regression is large enough to revive the free_list
    for bound methods?

    Yes!

  5. vstinner commented on Apr 28, 2020

    @vstinner
    Member

    read_boundmethod 27.7 ns 47.1 ns

    Extract of Tools/scripts/var_access_benchmark.py:

    def read_boundmethod(trials=trials, a=A()):
        for t in trials:
            a.m;    a.m;    a.m;    a.m;    a.m
            a.m;    a.m;    a.m;    a.m;    a.m
            a.m;    a.m;    a.m;    a.m;    a.m
            a.m;    a.m;    a.m;    a.m;    a.m
            a.m;    a.m;    a.m;    a.m;    a.m

    Which kind of code pattern is impacted by this performance regression, apart this micro-benchmark? Do you notice a significant slowdown in pyperformance?

    When pyperformance was run before the change was merged, there was no significant difference:
    https://bugs.python.org/issue37340#msg348425

    In bpo-37340, you wrote that sorted(data, key=str.upper) is 70% slower. Would you mind to provide the benchmark?

  6. vstinner commented on Apr 28, 2020

    @vstinner
    Member

    I compared sorted(data, key=str.upper) performance between before the removal of the free list (parent of commit 3e54b57) and the current master branch, I get:

    Mean +- std dev: [master] 167 us +- 4 us -> [cache] 179 us +- 7 us: 1.07x slower (+7%)

    I cannot reproduce the "70% slowdown".

    Raymond: How did you compile Python? What is your OS? How did you run your benchmark?

    ---

    It's a quick & dirty benchmark run. I didn't use LTO+PGO and I didn't isolate my CPU. Maybe the difference of 12 ns is just caused by the noise of my coarse benchmark procedure.

    I used commands:

    # master
    git checkout master
    git clean -fdx
    ./configure && make && ./python -m venv env && env/bin/python -m pip install pyperf
    env/bin/python -m pyperf timeit -s 'import random; data = [f"x{i}" for i in range(1000)]; random.shuffle(data)' 'sorted(data, key=str.upper)' -o ../master.json

    # before removal of the free list
    git checkout 3e54b57^
    git clean -fdx
    ./configure && make && ./python -m venv env && env/bin/python -m pip install pyperf
    env/bin/python -m pyperf timeit -s 'import random; data = [f"x{i}" for i in range(1000)]; random.shuffle(data)' 'sorted(data, key=str.upper)' -o ../master.json

    That's a Fedora 31 using GCC -O3 (GCC 9.3.1).

    --

    The commit before the removal of the free list is:

    commit 76b6451 (HEAD)
    Date: Fri Jul 26 03:30:33 2019 +0200

    Well, maybe I did a mistake.

  7. vstinner commented on Apr 28, 2020

    @vstinner
    Member

    By the way, in sorted(data, key=str.upper): str.upper is an unbound method, no?

    $ ./python
    Python 3.9.0a6+ (heads/master:d9a43e20fa, Apr 28 2020, 23:50:37) 
    >>> type(str.upper)
    <class 'method_descriptor'>
  8. transferred this issue fromon Apr 10, 2022
  9. eendebakpt commented on Feb 1, 2026

    @eendebakpt
    Contributor

    The performance regression became a bit worse in 3.11 and improved a bit again in 3.14 (maybe because a freelist was added again #128692, although I did not measure).

    (fiftyone) c:\projects\tmp>uv run -p 3.8 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 62 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.9 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 86.3 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.10 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 84.7 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.11 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    2000000 loops, best of 11: 105 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.12 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    2000000 loops, best of 11: 107 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.13 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    2000000 loops, best of 11: 117 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.14 -m timeit -r 11 -s "class A: pass" -s "A.m = lambda s: None" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    2000000 loops, best of 11: 103 nsec per loop
    

    For normal attributes the result is different:

    (fiftyone) c:\projects\tmp>uv run -p 3.8 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    10000000 loops, best of 11: 37.6 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.9 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    10000000 loops, best of 11: 38.1 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.10 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 42.3 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.11 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 52 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.12 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    5000000 loops, best of 11: 49.5 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.13 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    10000000 loops, best of 11: 28.8 nsec per loop
    
    (fiftyone) c:\projects\tmp>uv run -p 3.14 -m timeit -r 11 -s "class A: pass" -s "A.m = 10" -s "a = A()" "a.m; a.m; a.m; a.m; a.m"
    10000000 loops, best of 11: 24.7 nsec per loop
    
  10. eendebakpt commented on Jun 19, 2026

    @eendebakpt
    Contributor

    I investigated a bit more:

    • Part of the slowdown is due to immortal objects (pep 683), but this only explains a small part
    • Freelists have been removed and added later. On PGO builds the effect is also minor
    • The method access a.m is indeed slower. There are some changes that made it slower (but these changes made other paths faster):
    1. Due to managed dicts we have one more pointer indirection
    2. The LOAD_ATTR AND LOAD_METHOD have been merged (Merge LOAD_METHOD back into LOAD_ATTR for better specialization #93429) increasing inline cache and some more handling

    We can restore performance with a dedicated opcode. For a microbenchmark (as in the OP) a.m is about 1.34x faster. This does require extra tier1 opcodes though (or tier2 only, but that pending on a possible jit pep).

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.10 (EOL)end of life3.11only security fixes3.12only security fixes3.9 (EOL)end of lifeinterpreter-core(Objects, Python, Grammar, and Parser dirs)performancePerformance or resource usage

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions