Repository navigation
Conversation
8623456 to
765d7d5
Compare
Obtain a reference to lz->long_cnt, lz->long_step such that we don't hit a use after free
765d7d5 to
8580b6a
Compare
|
This appears to add some very slight overhead to repr in slow mode, no impact to the likely more heavily used fast mode |
|
Performances are not really inportant for |
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
|
@picnixz would you be able to kindly review again, thanks! |
|
@picnixz kindly pining for a re-review, thank you! |
picnixz
left a comment
There was a problem hiding this comment.
It may just seem dumb but since __repr__ is fairly used just for debugging, I don't think we really need to worry about performances; so why not just having everything in a critical section? would that work?
|
Would that not risk locking other threads executing more latency sensitive functions that are holding the lock? |
|
We don't care here IMO. If you print the counter, just expect it to possbly be slow. And can you simply benchmark this please? |
|
If I add a lock around the whole of repr I can significantly slow down the actual counting module if another thread is printing. I'm sure this example is a bit contrived however I think a real possibility another thread printing could slow down the counting module.
simple_bench.py (see below) Detailsimport statistics
import threading
import time
from itertools import count
N = 1_000_000
def bench():
it = count(10**18, 2) # slow mode: next() takes the critical section
done = threading.Event()
elapsed = []
def counter():
start = time.perf_counter()
for _ in range(N):
next(it)
elapsed.append(time.perf_counter() - start)
def reprer():
while not done.is_set():
repr(it)
r = threading.Thread(target=reprer)
c = threading.Thread(target=counter)
r.start()
c.start()
c.join()
done.set()
r.join()
return elapsed[0]
times = [bench() for _ in range(5)]
print(f"next() thread: {statistics.median(times) * 1000:.1f} ms "
f"for {N:,} calls while another thread calls repr() (median of 5)")And just to confirm a lock held across repr would look something like this static PyObject *
count_repr_lock_held(countobject *lz)
{
if (lz->long_cnt == NULL) {
Py_ssize_t cnt = FT_ATOMIC_LOAD_SSIZE_RELAXED(lz->cnt);
return PyUnicode_FromFormat("%s(%zd)",
_PyType_Name(Py_TYPE(lz)), cnt);
}
if (PyLong_Check(lz->long_step)) {
long step = PyLong_AsLong(lz->long_step);
if (step == -1 && PyErr_Occurred()) {
PyErr_Clear();
}
if (step == 1) {
/* Don't display step when it is an integer equal to 1 */
return PyUnicode_FromFormat("%s(%R)",
_PyType_Name(Py_TYPE(lz)),
lz->long_cnt);
}
}
return PyUnicode_FromFormat("%s(%R, %R)",
_PyType_Name(Py_TYPE(lz)),
lz->long_cnt, lz->long_step);
}
static PyObject *
count_repr(PyObject *op)
{
PyObject *result;
Py_BEGIN_CRITICAL_SECTION(op);
result = count_repr_lock_held(countobject_CAST(op));
Py_END_CRITICAL_SECTION();
return result;
} |
|
Two possibilities:
|
|
I'm not sure if this was what you were after but I think this reduces the branches considerably if the if/else branches were the main sticking point. Agree looking back it was rather messy for all the approaches! |
| lz->long_cnt, lz->long_step); | ||
|
|
||
| /* Don't display step when it is an integer equal to 1 */ | ||
| msg = PyUnicode_FromFormat(step == 1 ? "%s(%R)" : "%s(%R, %R)", |
There was a problem hiding this comment.
You can put PyObject *msg = here instead.
| /* Don't display step when it is an integer equal to 1 */ | ||
| msg = PyUnicode_FromFormat(step == 1 ? "%s(%R)" : "%s(%R, %R)", | ||
| _PyType_Name(Py_TYPE(lz)), | ||
| long_cnt, lz->long_step); |
There was a problem hiding this comment.
Can we have the if check out of the format call? I don't exactly remember but I think passing more va args than what the format expects is UB>
Obtain a reference to lz->long_cnt, lz->long_step
such that we don't hit a use after free.