GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — _decimal: pi benchmark 31-45% slower compared to 3.9 · Issue #114682 · python/cpython

_decimal: pi benchmark 31-45% slower compared to 3.9 #114682

New issue
Copy link
New issue
Copy link
Closed as not planned
Closed as not planned
_decimal: pi benchmark 31-45% slower compared to 3.9#114682
Copy link
Labels
performancePerformance or resource usagePerformance or resource usagetopic-subinterpreterstype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

Description

@skrah
skrah
opened on Jan 28, 2024
Issue body actions

Bug report

Bug description:

The pi benchmark in Modules/_decimal/tests/bench.py is up to 31% slower with --enable-gil and up to 45% slower with --disable-gil compared to Python 3.9 as the baseline.

Change the number of iterations to 100000 for more stable benchmarks.

This is heavily OS and hardware dependent. The worst case cited above was observed on a six core i5-8600K.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

  • gh-114682: Use METH_METHOD calling convention in _decimal #115196
  • gh-114682: Improve performance of module state access in _decimal #115401
Reacted by Pieter Eendebak

Activity

Next
  1. Eclips4
    added
    performancePerformance or resource usagePerformance or resource usage
    on Jan 28, 2024
  2. sunmy2019 commented on Jan 29, 2024

    @sunmy2019
    sunmy2019
    on Jan 29, 2024
    Member
    More actions

    Can you try --disable-gil with current main?

  3. skrah commented on Jan 29, 2024

    @skrah
    skrah
    on Jan 29, 2024
    ContributorAuthor
    More actions

    The figures are almost the same, around 30% (enable) and 43% (disable).

  4. skrah commented on Jan 29, 2024

    @skrah
    skrah
    on Jan 29, 2024
    ContributorAuthor
    More actions

    The base slowdown of 30% is of course due to the module state changes. For comparison, and to see that the i5-8600K is not inherently underperforming with threads:

    The ideal situation is implemented in libmpdec++: It has a C++11 thread local context, which is also called for every inline operation. The C++11 TLS adds just 4% overhead compared to pure C (where the context is passed directly to the functions).

    This of course would be quite difficult to achieve in CPython. But the thread local context may not even be the primary culprit. In Martin von Loewis' first module state implementation the basic slowdown for _decimal was 20%, and a good part of that was due to the heap types.

    Reacted by sunmy2019
  5. skrah commented on Jan 29, 2024

    @skrah
    skrah
    on Jan 29, 2024
    ContributorAuthor
    More actions

    The ideal situation is implemented in libmpdec++: It has a C++11 thread local context, which is also called for every inline operation. The C++11 TLS adds just 4% overhead compared to pure C (where the context is passed directly to the functions).

    This of course would be quite difficult to achieve in CPython.

    The wording was not ideal: The key point is that C++ achieves this without a cached context, which is of course now in the module state.

  6. ericsnowcurrently commented on Feb 1, 2024

    @ericsnowcurrently
    ericsnowcurrently
    on Feb 1, 2024
    Member
    More actions

    @encukou, any thoughts on this?

  7. neonene commented on Feb 2, 2024

    @neonene
    neonene
    on Feb 2, 2024
    Contributor
    More actions

    PyModule_GetState() Getting a module state via PyType_GetModuleByDef() seems to be expensive for heavy use.
    It would be nice if a heap type could own the module state's address at its creation time to recover more than 10%.

    Reacted by Erlend E. Aasland
  8. erlend-aasland commented on Feb 2, 2024

    @erlend-aasland
    erlend-aasland
    on Feb 2, 2024
    Contributor
    More actions

    PyModule_GetState() via PyType_GetModuleByDef() seems to be expensive for heavy use. It would be nice if a heap type could own the module state's address at its creation time to recover more than 10%.

    _PyModule_GetState() is used, not PyModule_GetState(); I'd be surprised if this is the culprit:

    cpython/Include/internal/pycore_moduleobject.h

    Lines 32 to 35 in 1aec064

    static inline void* _PyModule_GetState(PyObject* mod) {
    assert(PyModule_Check(mod));
    return ((PyModuleObject *)mod)->md_state;
    }

    OTOH, PyType_GetModuleByDef() can be more expensive. Storing the state pointer in the type struct is a possibility.

  9. neonene commented on Feb 2, 2024

    @neonene
    neonene
    on Feb 2, 2024
    Contributor
    More actions

    @ericsnowcurrently Can a module state be accessed correctly with the following?

    _Py_thread_local module_state *thread_local_state; // set at the exec phase

    If not invalid, it will be useful for the function which currently has no way to get the module state. A _Py_thread_local pointer may not be so fast to access, but it can also bypass a state struct?

  10. ericsnowcurrently commented on Feb 2, 2024

    @ericsnowcurrently
    ericsnowcurrently
    on Feb 2, 2024
    Member
    More actions

    Multiple interpreters can run in the same thread, so a thread-local may be ambiguous in that case.


    As to storing a reference to the module state on the type object, that shouldn't be necessary. Every heap type as a ht_module field that points to the module, accessible via PyType_GetModule(). From there you call PyModule_GetState(). In fact, we already have PyType_GetModuleState() that combines those two calls. That's fairly efficient, though obviously not as efficient as a static global.

    The only caveat is that PyType_GetModule() (and thus PyType_GetModuleState()) operates relative to the given type. Thus subclasses make things trickier. IIRC, when they're involved, you end up having to walk the MRO one way or another, which is what PyType_GetModuleByDef() does.

    FWIW, the common case where PyType_GetModuleByDef() is used is either in "tp slot" functions or intermediate static functions that aren't doing the right thing. In both cases the problem lies in throwing away the expensive information we already have. For tp slot functions the API doesn't pass the module in, even though it could. We're stuck with backward compatibility issues there. For intermediate static functions, I expect it's all too common to figure out the module (or module state) at some "leaf node" function but not pass it through the intermediate functions. Thus the relatively expensive PyType_GetModuleByDef() gets called more than it needs to be.

    I suppose another problem case is with callbacks that don't provide a way to pass the module through. I'm not sure that's a common issue though.

    Back to the _decimal module, I haven't looked at what usage patterns are resulting in such a significant slowdown.

  11. erlend-aasland commented on Feb 2, 2024

    @erlend-aasland
    erlend-aasland
    on Feb 2, 2024
    Contributor
    More actions

    Thus subclasses make things trickier. IIRC, when they're involved, you end up having to walk the MRO one way or another, which is what PyType_GetModuleByDef() does.

    Yes; heap types that are not base classes can go the fast route via PyType_GetModuleState() (and _PyType_GetModuleState()).

  12. encukou commented on Feb 4, 2024

    @encukou
    encukou
    on Feb 4, 2024 via email · Hidden as off-topic
    show commentMore actions
  13. hauntsaninja commented on Feb 4, 2024

    @hauntsaninja
    hauntsaninja
    on Feb 4, 2024 · Hidden as resolved
    show commentMore actions
  14. skrah commented on Feb 4, 2024

    @skrah
    skrah
    on Feb 4, 2024
    ContributorAuthor
    More actions

    The float object gets the luxury of its own freelist and avoids the slow PyObject_GC_* functions altogether. The Decimal constructor is a pure function (the context is only used for recording errors) and Decimals are immutable, just like floats.

    It is likely that 15 percentage points can be shaved off just by doing the same as in floatobject.c and then focus on the thread local context (indeed, PyType_GetModuleByDef() is called at dozens of places).

    Reacted by neonene
  15. erlend-aasland commented on Feb 4, 2024

    @erlend-aasland
    erlend-aasland
    on Feb 4, 2024
    Contributor
    More actions

    AFAICS, we can trim away a lot of the PyType_GetModuleByDef calls. For example, a lot of them are happening in methods; we can use the METH_METHOD calling convention on practically all of these, so we can access the module state using _PyType_GetModuleState:

    cpython/Include/internal/pycore_typeobject.h

    Lines 100 to 109 in 15f6f04

    _PyType_GetModuleState(PyTypeObject *type)
    {
    assert(PyType_Check(type));
    assert(type->tp_flags & Py_TPFLAGS_HEAPTYPE);
    PyHeapTypeObject *et = (PyHeapTypeObject *)type;
    assert(et->ht_module);
    PyModuleObject *mod = (PyModuleObject *)(et->ht_module);
    assert(mod != NULL);
    return mod->md_state;
    }

    Also (but not performance relevant), the find_state_left_or_right() calls in the Py_tp_richcompare slots can be replaced with get_module_state_by_def() calls.

    Reacted by neonene and Petr Viktorin
  16. 23 remaining items

  17. skrah commented on Feb 11, 2024

    @skrah
    skrah
    on Feb 11, 2024
    ContributorAuthor
    More actions

    I'm getting a smaller speedup by using PyType_GetModuleState() in the number methods and a larger one by using PyObject_New for the decimal constructor.

    The _threadmodule itself uses PyObject_New in new_thread_handle.

    @ericsnowcurrently, is there a reason why PyObject_New cannot be used for the immutable Decimal type?

  18. vstinner commented on Feb 12, 2024

    @vstinner
    vstinner
    on Feb 12, 2024
    Member
    More actions

    @erlend-aasland:

    I withdraw myself from this discussion; I am no longer working on this issue.

    @skrah:

    That is an extraordinary comment from Mr. Stinner. It is entirely irrelevant to this issue

    Well, same than Erlend for me: I unsubscribe. I'm not interested to work on an issue with such tone in the discussion.

    Reacted by Erlend E. Aasland, Maxim Martynov and Gregory P. Smith
  19. ericsnowcurrently commented on Feb 12, 2024

    @ericsnowcurrently
    ericsnowcurrently
    on Feb 12, 2024
    Member
    More actions

    is there a reason why PyObject_New cannot be used for the immutable Decimal type?

    I'm not aware of any reason. It isn't a GC type, right?

  20. skrah commented on Feb 12, 2024

    @skrah
    skrah
    on Feb 12, 2024
    ContributorAuthor
    More actions

    I'm not aware of any reason. It isn't a GC type, right?

    Yes, up to 3.12 it wasn't a GC type. It has been made a GC type in main during the module state changes.

    I now found some explanations in PEP-630, but I still do not understand it:

    Instances of heap types hold a reference to their type.
    This ensures that the type isn't destroyed before all its instances are,
    but may result in reference cycles that need to be broken by the
    garbage collector.
    

    How would one create a cycle?

    >>> x = Decimal("3.222")
    >>> x.y = x
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        x.y = x
        ^^^
    AttributeError: 'decimal.Decimal' object has no attribute 'y' and no __dict__ for setting new attributes
    
    >>> x = Decimal("3.222")
    >>> t = type(x)
    >>> t.y = x
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        t.y = x
        ^^^
    TypeError: cannot set 'y' attribute of immutable type 'decimal.Decimal'
    
    >>> x.adjusted = 10
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
        x.adjusted = 10
        ^^^^^^^^^^
    AttributeError: 'decimal.Decimal' object attribute 'adjusted' is read-only
    
  21. neonene commented on Feb 13, 2024

    @neonene
    neonene
    on Feb 13, 2024
    Contributor
    More actions

    The strategy suggested earlier of caching the state in the decimal object seems to be the most fruitful (intuitively, the actual patch will require some experimentation).

    @skrah I have posted a draft: gh-115401

  22. ericsnowcurrently commented on Feb 13, 2024

    @ericsnowcurrently
    ericsnowcurrently
    on Feb 13, 2024
    Member
    More actions

    Yes, up to 3.12 it wasn't a GC type. It has been made a GC type in main during the module state changes.

    Ah, there are definitely some subtleties involved with GC types, especially when switching from a non-GC type. I don't recall the specifics, but I'm sure @encukou does.

  23. encukou commented on Feb 14, 2024

    @encukou
    encukou
    on Feb 14, 2024
    Member
    More actions

    It might be more interesting to start with static (non-GC) types: these are “immortal” (statically allocated), but they do have a bunch of dynamically allocated, interpreter-specific data (e.g. subclasses, weakrefs). Getting this data properly cleaned up requires rather special hacks, appropriate for core types that really are created/destroyed with the interpreter itself, rather than extensions loaded on demand (see code around static_types[] in Objects/object.c for details).
    On the other hand, GC types (heap types) use reference counting like any other Python object. Not saying that getting rid of all the assumptions of static-ness/immortality is/was smooth, but at least the model is much clearer: we have a good idea of what the details should be, even though getting there is a bumpy road full of backcompat issues.


    Instances of heap types hold a reference to their type.
    This ensures that the type isn't destroyed before all its instances are,
    but may result in reference cycles that need to be broken by the
    garbage collector.

    This is a generic statement. A class with no __dict__ nor user-settable attributes probably can't create a reference cycle.
    While we could omit the instance→type reference only in cases where one can prove this, I don't think such a special case would be worth it.

  24. neonene commented on Feb 16, 2024

    @neonene
    neonene
    on Feb 16, 2024
    Contributor
    More actions

    How would one create a cycle?

    PEP 573 and type_traverse() says:

    Usually, creating a class with ht_module set will create
    a reference cycle involving the class and the module.
    

    cpython/Objects/typeobject.c

    Lines 5526 to 5534 in 20eaf4d

    Py_VISIT(type->tp_dict);
    Py_VISIT(type->tp_cache);
    Py_VISIT(type->tp_mro);
    Py_VISIT(type->tp_bases);
    Py_VISIT(type->tp_base);
    Py_VISIT(((PyHeapTypeObject *)type)->ht_module);
    /* There's no need to visit others because they can't be involved
    in cycles:

    To make Decimal type a non-GC heap type, at least the module arg needs to be NULL in PyType_FromMetaclass(), like thread_handle_type in _threadmodule?

    I agree that PyObject_GC_*() has some overhead, but I'm not sure how to hold a module safely outside Decimal type.

  25. ericsnowcurrently
    added
    topic-subinterpreters
    on Feb 27, 2024
  26. github-project-automation
    added this to Subinterpreterson Feb 27, 2024
  27. github-project-automation
    moved this to Todo in Subinterpreterson Feb 27, 2024
  28. gpshead commented on Mar 12, 2024

    @gpshead
    gpshead
    on Mar 12, 2024
    Member
    More actions

    FYI - skrah has been banned from python spaces, quoting details from my message posted on a duplicate-ish issue:
    #114922 (comment)

    """
    Procedural comment: For all involved on this issue, @skrah was banned from Python spaces per a Conduct Working Group recommendation to the Steering Council in 2020. A configuration oversight on our part in 2020 made that not so on Github. The intended ban of @skrah was reinstated on 2024-02-22. Our apologies to all for the consequences of this mixup (it was the first core dev ban done on our modern systems, we now understand how and why mistakes were made).

    Stefan was informed of this privately via email and re-offered the opportunity to apologize for past actions and to accept our code of conduct going forwards per the original 2020 ban's terms and notification. We have not received a reply.

    I'm posting this here as we view replying in the same forum where issues recently surfaced a reasonable way to notify past, present, and potential future participants of an incident response.


    This says nothing for or against the validity of this issue. A new issue should be opened if people wish to proceed. I'm locking the comments on this one. For information on what the Github concept of a ban means, read the Github org ban docs.

    -- The Python Steering Council, 2024 edition
    ""

  29. gpshead
    closed this as not plannedon Mar 12, 2024
  30. github-project-automation
    moved this from Todo to Done in Subinterpreterson Mar 12, 2024
  31. python
    locked as too heated and limited conversation to collaborators on Mar 12, 2024
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

    performancePerformance or resource usagePerformance or resource usagetopic-subinterpreterstype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error

    Projects

    • Subinterpreters
      Status
      Done
      Show more project fields

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    You are visitor 01402520 — GeezerVison does not store the pages you look up.
    Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.