Conversation
Co-authored-by: Ilya Egorov <0x42005e1f@gmail.com>
|
cc @x42005e1f @ZeroIntensity @picnixz Here is the data (based on unittests, bm_nbody, bm_fannkuch), why I choose this way.
And here is the microbenchmark Free-threadingmain vs this PR
Benchmark hidden because not significant (4): extend+del[S:] S=8, extend+del[S:] S=1024, extend+del[S:] S=4096, copy only N=1024 main vs #158602 alone
Default buildmain vs this PR
Benchmark hidden because not significant (9): extend+del[S:] S=4, S=8, S=32, S=64, S=256, S=1024, copy only N=64, N=1024, N=4096 main vs #158602 alone
Benchmark hidden because not significant (17): append+pop size=1, 3, 15, 100; append+del[-1] size=15; extend+del[S:] S=4, 16, 64, 256, 1024, 4096; copy only N=32, 64, 256, 1024, 4096, 65536 Notes on the default build: Scriptimport pyperf
INNER = 1000
def append_del_last(obj, inner=INNER):
for _ in range(inner):
obj.append(None)
del obj[-1]
def append_pop(obj, inner=INNER):
for _ in range(inner):
obj.append(None)
obj.pop()
def extend_del_slice(obj, blk, size, inner):
for _ in range(inner):
obj.extend(blk)
del obj[size:]
def copy_only(blk):
obj = blk[:]
return obj
def del_last_repeatedly(blk, r):
obj = blk[:]
for _ in r:
del obj[-1]
return obj
def del_first_repeatedly(blk, r):
obj = blk[:]
for _ in r:
del obj[0]
return obj
def main():
runner = pyperf.Runner()
for size in (0, 1, 3, 7, 15, 100):
obj = [None] * size
runner.bench_func(f"append+del[-1] size={size}", append_del_last,
obj, inner_loops=INNER)
obj = [None] * size
runner.bench_func(f"append+pop size={size}", append_pop,
obj, inner_loops=INNER)
for size in (1, 2, 4, 8, 16, 32, 64, 256, 1024, 4096):
inner = 100 if size <= 64 else 10
obj = [None] * size
blk = [None] * size
runner.bench_func(f"extend+del[S:] S={size}", extend_del_slice,
obj, blk, size, inner, inner_loops=inner)
for n in (32, 64, 256, 1024, 4096, 65536):
blk = list(range(n))
r = range(n - 1)
runner.bench_func(f"copy only N={n}", copy_only, blk)
runner.bench_func(f"del[-1] x N N={n}", del_last_repeatedly, blk, r)
for n in (256, 4096):
blk = list(range(n))
r = range(n - 1)
runner.bench_func(f"del[0] x N N={n}", del_first_repeatedly, blk, r)
if __name__ == "__main__":
main()A nice side effect of this approach is that |
| FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item[idx], a->ob_item[idx + 1]); | ||
| } | ||
| Py_SET_SIZE(a, size - 1); | ||
| list_resize(a, size - 1); // NB: shrinking a list can't fail |
There was a problem hiding this comment.
Here, you are doing the same thing I did in my PR. Please do not do this: to me, it does not seem very ethical. Keep exactly what the title says: the list_resize() tuning.
There was a problem hiding this comment.
@x42005e1f This is the reason why I added you as co-authored of this patch. But your initial proposal would be very difficult to accept because of performance issue. To supplement your patch, I spent most of time for the data analysis.
There was a problem hiding this comment.
But your initial proposal would be very difficult to accept because of performance issue.
In that case, why not merge this PR without the fix first, and then mine, or vice versa? What is the problem? You do not even have a NEWS entry about the fix. I even mentioned a possible tuning of list_resize() in the description of my PR, but I did not make the corresponding changes because that goes beyond the scope of this specific fix.
There was a problem hiding this comment.
We can do that if you want. But for the performance optimization, we need to understand how it worth to do for especially for this kind of performance regression issue.
I am fine with merging this PR after your PR is merged.
This is part of effort to pursuade your issue is worth to fix.
There was a problem hiding this comment.
Better communication for this kind of issue is that
- Simply request to add more detail or your credit to NEWS.d (Sorry I forgot to mention this even I added you as co-author in commit log)
or - Request to merge this PR after your PR is merged.
There was a problem hiding this comment.
To me, the second option seems preferable, so I am requesting it.
There was a problem hiding this comment.
In anycase, we need to wait until other core devs like this approach. So please be patient.
There was a problem hiding this comment.
imo. we should not merge a PR that we do not prefer and then merge a fix straight after. We could pull the changes here (or whatever changes we prefer in the end) into the first open branch though.
|
I prefer this approach. How many bytes exactly are we allocating for a list of size N? having small list size N=16 seems arbitrary but if we have N=32, we could mitigate more cases such as Otherwise, it's ok for N=16 to be the threshold. (1.2 slower is not THAT bad eventually but this could become bad when users create lots of small lists and pop them entirely). |
|
And why not take a look at the point I made regarding the 1-2 items case in #158602 (comment)? If you had avoided those unnecessary reallocations when |
|
@x42005e1f please keep your comments respectful and constructive, and make sure your interactions follow the Python Code of Conduct. If this kind of behaviour continues, we may have to restrict your ability to contribute. |
IIUC, allocated * 8.
If we want to be memory frugal, I think that 16 is the best value when we run unit tests and several benchmarks in pyperformance (97.2% of shrink requests are covered). I am not sure that we need to cover the remaining 2% since those reallocs actually reclaim memory. Do you want to increase this to 32? |
I think everything is a trade-off here, we would lose too much performance to be frugal with very tiny lists. |
Could you please explain? An exact comparison of integers is a fairly inexpensive operation (relative to reallocation), and skipping the reallocation is clearly cheaper than reallocating for the same capacity. Remember, Lines 129 to 137 in f610e8c Modeling for >>> for newsize in range(16):
... new_allocated = (newsize + (newsize >> 3) + 6) & ~3
... if newsize - (newsize + 1) > new_allocated - newsize:
... new_allocated = (newsize + 3) & ~3
... if newsize == 0:
... new_allocated = 0
... print(f"{newsize} -> {new_allocated}")
0 -> 0
1 -> 4 # newsize < 2
2 -> 8 # newsize < 4
3 -> 8 # newsize < 4
4 -> 8
5 -> 8
6 -> 12
7 -> 12
8 -> 12
9 -> 16
10 -> 16
11 -> 16
12 -> 16
13 -> 20
14 -> 20
15 -> 20If we skip the reallocation for Although, yes, I agree. It covers too few cases and is redundant given your changes (but sufficient without them, for >1.15x slower in the last table; the case where |
|
My "trade-off" comment was about calling |
True for the benchmark run, but in the measured distribution the check still leaves more than 40% of shrink requests reallocating. So I think that the current approach is better. |
|
I'm much more comfortable with this fix. 3-7% slower is a lot easier to swallow than the 160% from the other PR. |
| if (newsize == 0) | ||
| new_allocated = 0; | ||
|
|
||
| // gh-158602: when shrinking, do not reallocate the array of a small list. |
There was a problem hiding this comment.
This could also be inline in list_ass_item_lock_held. The performance regression is a bit less then (depending on workloads).
| FT_ATOMIC_STORE_PTR_RELEASE(a->ob_item[idx], a->ob_item[idx + 1]); | ||
| } | ||
| Py_SET_SIZE(a, size - 1); | ||
| list_resize(a, size - 1); // NB: shrinking a list can't fail |
There was a problem hiding this comment.
imo. we should not merge a PR that we do not prefer and then merge a fix straight after. We could pull the changes here (or whatever changes we prefer in the end) into the first open branch though.
|
|
||
| // gh-158602: when shrinking, do not reallocate the array of a small list. | ||
| if (newsize < allocated) { | ||
| if (allocated <= LIST_SMALL_ALLOCATED) { |
There was a problem hiding this comment.
This check can be folded into the first check (e.g. the one below /* Bypass realloc() when a previous overallocation
del list[index]never reallocates the array #158592