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
_decimal: pi benchmark 31-45% slower compared to 3.9 #114682
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.
So, here's my suggested approach. Let's move the relevant parts of bench.py over to pyperformance. That way, it is easier to keep an eye on performance regressions like this; the faster-cpython team is alert to sudden changes in a particular benchmark.
I'll try to tear out as many PyType_GetModuleByDef as possible and create a PR, hopefully within this week. The diff is going to be huge, but OTOH it is a mechanical change.
Let me know if you need any help, and feel free to assign me to reviews.
Perhaps you could migrate bench.py over to pyperformance?
For now, I've used this patch for local benchmarking with pyperf1:
Details
diff --git a/Modules/_decimal/tests/bench.py b/Modules/_decimal/tests/bench.py
index 640290f2ec..6b6117f108 100644
--- a/Modules/_decimal/tests/bench.py+++ b/Modules/_decimal/tests/bench.py@@ -11,9 +11,14 @@
from functools import wraps
from test.support.import_helper import import_fresh_module
+import pyperf++
C = import_fresh_module('decimal', fresh=['_decimal'])
P = import_fresh_module('decimal', blocked=['_decimal'])
+runner = pyperf.Runner()+
#
# NOTE: This is the pi function from the decimal documentation, modified
# for benchmarking purposes. Since floats do not have a context, the higher
@@ -81,33 +86,20 @@ def wrapper(*args, **kwargs):
return _increase_int_max_str_digits
def test_calc_pi():
- print("\n# ======================================================================")- print("# Calculating pi, 10000 iterations")- print("# ======================================================================\n")-
to_benchmark = [pi_float, pi_decimal]
if C is not None:
to_benchmark.insert(1, pi_cdecimal)
for prec in [9, 19]:
- print("\nPrecision: %d decimal digits\n" % prec)
for func in to_benchmark:
- start = time.time()
if C is not None:
C.getcontext().prec = prec
P.getcontext().prec = prec
- for i in range(10000):- x = func()- print("%s:" % func.__name__.replace("pi_", ""))- print("result: %s" % str(x))- print("time: %fs\n" % (time.time()-start))+ name = f"{func.__name__}, precision: {prec} decimal digits"+ runner.bench_func(name, func)
@increase_int_max_str_digits(maxdigits=10000000)
def test_factorial():
- print("\n# ======================================================================")- print("# Factorial")- print("# ======================================================================\n")-
if C is not None:
c = C.getcontext()
c.prec = C.MAX_PREC
@@ -147,4 +139,4 @@ def test_factorial():
if __name__ == "__main__":
test_calc_pi()
- test_factorial()+ #test_factorial()
@skrah Do you have a significant regression test case where subclasses are frequently used? 3.12 (sigle-phase init/static types) was almost as slow as main in the given test for me once D = C.Decimal was replaced with class D(C.Decimal).
@skrah Do you have a significant regression test case where subclasses are frequently used? 3.12 (sigle-phase init/static types) was almost as slow as main in the given test for me once D = C.Decimal was replaced with class D(C.Decimal).
PyType_GetModuleByDef is slower for subclasses. Reducing the number of PyType_GetModuleByDef will have an impact.
@neonene Your test is reasonable! Subclasses are so slow that within 3.9 thatfloat is slowed down by 3.3 times and _decimal by 2.2 times.
The slowdown for subclasses within main is something like 4.5 times for float and 1.9 times for decimal. That seems to suggest, as I mentioned earlier, that certain things are better optimized for base class floats than for base class Decimals.
But in general, compared to 3.9, subclasses themselves seem to have gotten faster in main, so all comparisons are a bit messy.
Regarding the module-state-access after METH_METHOD(PEP 573) is applied to _decimal's methods, the concerns will move to hot tp_* functions, if any. A possible optimization would be:
PyObject*module=PyType_GetModule(type);
if (!module||_PyModule_GetDef(module) !=module_def) {
PyErr_Clear();
// Subclasses can ignore the overhead for now? Otherwise,// the type should cache the module state (refcount-free) with the def.module=PyType_GetModuleByDef(type, module_def);
}
module_state*state=_PyModule_GetState(module);
Currently, a faster version of PyType_GetModule() is not offered, right?
Regarding the module-state-access after METH_METHOD (PEP 573) is applied to decimal's methods, the concerns will move to hot tp* functions, if any.
In 2018, I wrote a change to use FASTCALL calling convention which made the telco benchmark 22% faster. But Stefan Krah was the module maintainer and was against the idea, so I gave up. See issue gh-73487.
PyType_GetModuleByDef is slower for subclasses. Reducing the number of PyType_GetModuleByDef will have an impact.
If PyType_GetModuleByDef() overhead (compared to the previous version which didn't use it) is too high, we can cache directly important data (such as decimal_state *state) in instances (ex: PyDecObject), rather than getting it from the instance type.
[...] we can cache directly important data (such as decimal_state *state) in instances (ex: PyDecObject), rather than getting it from the instance type.
Yes, I already suggested it; we do this in the sqlite3 extension module with great success.
In 2018, I wrote a change to use FASTCALL calling convention which made the telco benchmark 22% faster. But Stefan Krah was the module maintainer and was against the idea, so I gave up. See issue gh-73487.
I see Serhiy's post point to a bpo post where Stefan disapproved of applying Argument Clinic only. Well, I used Argument Clinic extensively in my patch for speeding up _decimal; it did not break a single test ;)
If PyType_GetModuleByDef() overhead (compared to the previous version which didn't use it) is too high, we can cache directly important data (such as decimal_state *state) in instances (ex: PyDecObject), rather than getting it from the instance type.
The cache in a known object is mainly used to reduce function arguments in the call chains, right? Indeed, many PyType_GetModuleByDef() can be removed there.
I think a cache in the type instance can be used in the tp slot functions where PyType_GetModuleByDef() needs to be placed to check(exact) the given object's type through a module state, which seems not to be beneficial for the current very slow subclasses.
I think we can measure/consider the boost by FASTCALL separately.
@skrahMETH_METHOD|METH_FASTCALL might be (worse than) neutral on the pi test. I'm leaving an experimental PR #115196, which was my first AC experience.
@neonene Thanks for doing this! AC does not seem to affect the number methods (nb_add etc.) used in the pi benchmark. So it is indeed entirely irrelevant to this issue (as you hinted at).
I cannot reproduce the stated 22% improvement for the telco benchmark (telco.py full) either. I get something around 10%.
If you want to work on a patch without AC, I can review it. 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).
Generally, it would be good to have a reference module in Python core that does not use AC. AC is unused/not available in the vast majority of C-extensions. Many modules in the scientific ecosystem may want to use module state and vectorcall but not AC.
I got no performance change by switching to _PyType_GetModuleState() in the *_impl functions: 1eeb531. As to PEP-573, applying METH_METHOD only to hot methods would be enough, if any.
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?
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
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).
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.
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.
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.
Activity
erlend-aasland commentedon Feb 5, 2024
FTR: I did a quick proof-of-concept for some low-hanging fruit, and I already got a
4%10%12% speedup.erlend-aasland commentedon Feb 5, 2024
So, here's my suggested approach. Let's move the relevant parts of bench.py over to pyperformance. That way, it is easier to keep an eye on performance regressions like this; the faster-cpython team is alert to sudden changes in a particular benchmark.
I'll try to tear out as many
PyType_GetModuleByDefas possible and create a PR, hopefully within this week. The diff is going to be huge, but OTOH it is a mechanical change.encukou commentedon Feb 5, 2024
Thank you for getting to this so fast!
Let me know if you need any help, and feel free to assign me to reviews.
erlend-aasland commentedon Feb 5, 2024
Perhaps you could migrate bench.py over to pyperformance?
For now, I've used this patch for local benchmarking with pyperf1:
Details
Footnotes
for the pi benchmark only! ↩
neonene commentedon Feb 6, 2024
@skrah Do you have a significant regression test case where subclasses are frequently used? 3.12 (sigle-phase init/static types) was almost as slow as main in the given test for me once
D = C.Decimalwas replaced withclass D(C.Decimal).erlend-aasland commentedon Feb 6, 2024
PyType_GetModuleByDefis slower for subclasses. Reducing the number ofPyType_GetModuleByDefwill have an impact.skrah commentedon Feb 6, 2024
@neonene Your test is reasonable! Subclasses are so slow that within 3.9 that
floatis slowed down by 3.3 times and_decimalby 2.2 times.The slowdown for subclasses within main is something like 4.5 times for float and 1.9 times for decimal. That seems to suggest, as I mentioned earlier, that certain things are better optimized for base class
floatsthan for base classDecimals.But in general, compared to 3.9, subclasses themselves seem to have gotten faster in main, so all comparisons are a bit messy.
neonene commentedon Feb 6, 2024
Regarding the module-state-access after
METH_METHOD(PEP 573) is applied to_decimal's methods, the concerns will move to hottp_*functions, if any. A possible optimization would be:Currently, a faster version of
PyType_GetModule()is not offered, right?vstinner commentedon Feb 7, 2024
In 2018, I wrote a change to use FASTCALL calling convention which made the telco benchmark 22% faster. But Stefan Krah was the module maintainer and was against the idea, so I gave up. See issue gh-73487.
vstinner commentedon Feb 7, 2024
If PyType_GetModuleByDef() overhead (compared to the previous version which didn't use it) is too high, we can cache directly important data (such as
decimal_state *state) in instances (ex:PyDecObject), rather than getting it from the instance type.erlend-aasland commentedon Feb 7, 2024
Yes, I already suggested it; we do this in the sqlite3 extension module with great success.
erlend-aasland commentedon Feb 7, 2024
I see Serhiy's post point to a bpo post where Stefan disapproved of applying Argument Clinic only. Well, I used Argument Clinic extensively in my patch for speeding up
_decimal; it did not break a single test ;)encukou commentedon Feb 7, 2024
That would be using
PyHeapTypeObject.ht_moduledirectly. If we know it's heap type, the check forPy_TPFLAGS_HEAPTYPEcan become an assert.skrah commentedon Feb 7, 2024
That is an extraordinary comment from Mr. Stinner. It is entirely irrelevant to this issue:
This issue is about the slowdown introduced by the module state changes.
If AC speeds up
_decimalin the fix, that speedup just camouflages the module state slowdown and makes honest benchmarks for this issue impossible!In the linked FASTCALL issue, both @rhettinger and ultimately Mr. Stinner himself concurred with me or at least acquiesced.
At that time the new FASTCALL convention was already on the horizon and would have required another change!
At that time I planned to use the new FASTCALL convention directly.
I was not the "module maintainer", but the author. Big difference.
In short, bringing up a heavily editorialized version of the actual events just distracts from the problems at hand.
erlend-aasland commentedon Feb 7, 2024
I withdraw myself from this discussion; I am no longer working on this issue.
neonene commentedon Feb 7, 2024
The cache in a known object is mainly used to reduce function arguments in the call chains, right? Indeed, many
PyType_GetModuleByDef()can be removed there.I think a cache in the type instance can be used in the tp slot functions where
PyType_GetModuleByDef()needs to be placed to check(exact) the given object's type through a module state, which seems not to be beneficial for the current very slow subclasses.I think we can measure/consider the boost by
FASTCALLseparately.vstinner commentedon Feb 7, 2024
I reacted to a comment about METH_METHOD. For me, a common way to use METH_METHOD is to use Argument Clinic with
cls: defining_class.neonene commentedon Feb 9, 2024
@skrah
METH_METHOD|METH_FASTCALLmight be (worse than) neutral on the pi test. I'm leaving an experimental PR #115196, which was my first AC experience.skrah commentedon Feb 9, 2024
@neonene Thanks for doing this! AC does not seem to affect the number methods (
nb_addetc.) used in the pi benchmark. So it is indeed entirely irrelevant to this issue (as you hinted at).I cannot reproduce the stated 22% improvement for the telco benchmark (telco.py full) either. I get something around 10%.
If you want to work on a patch without AC, I can review it. 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).
Generally, it would be good to have a reference module in Python core that does not use AC. AC is unused/not available in the vast majority of C-extensions. Many modules in the scientific ecosystem may want to use module state and vectorcall but not AC.
neonene commentedon Feb 10, 2024
neonene commentedon Feb 10, 2024
I take back.
*_impl(self, cls)has in the module several callers which previously passed aNULLas a dummy.neonene commentedon Feb 10, 2024
I got no performance change by switching to
_PyType_GetModuleState()in the*_implfunctions: 1eeb531. As to PEP-573, applyingMETH_METHODonly to hot methods would be enough, if any.neonene commentedon Feb 10, 2024
Module state access related? functions:
Details
skrah commentedon Feb 11, 2024
I'm getting a smaller speedup by using
PyType_GetModuleState()in the number methods and a larger one by usingPyObject_Newfor the decimal constructor.The
_threadmoduleitself usesPyObject_Newinnew_thread_handle.@ericsnowcurrently, is there a reason why
PyObject_Newcannot be used for the immutableDecimaltype?vstinner commentedon Feb 12, 2024
@erlend-aasland:
@skrah:
Well, same than Erlend for me: I unsubscribe. I'm not interested to work on an issue with such tone in the discussion.
ericsnowcurrently commentedon Feb 12, 2024
I'm not aware of any reason. It isn't a GC type, right?
skrah commentedon Feb 12, 2024
Yes, up to
3.12it wasn't a GC type. It has been made a GC type inmainduring the module state changes.I now found some explanations in PEP-630, but I still do not understand it:
How would one create a cycle?
neonene commentedon Feb 13, 2024
@skrah I have posted a draft: gh-115401
ericsnowcurrently commentedon Feb 13, 2024
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.
encukou commentedon Feb 14, 2024
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[]inObjects/object.cfor 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.
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.
neonene commentedon Feb 16, 2024
PEP 573 and
type_traverse()says:cpython/Objects/typeobject.c
Lines 5526 to 5534 in 20eaf4d
To make
Decimaltype a non-GC heap type, at least themodulearg needs to beNULLinPyType_FromMetaclass(), likethread_handle_typein_threadmodule?I agree that
PyObject_GC_*()has some overhead, but I'm not sure how to hold a module safely outsideDecimaltype.gpshead commentedon Mar 12, 2024
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
""