Skip to content

gh-158592: Skip reallocation when shrinking a small list - #158787

Open
corona10 wants to merge 6 commits into
python:mainfrom
corona10:gh-158592
Open

corona10 wants to merge 6 commits into
python:mainfrom
corona10:gh-158592

Conversation

@corona10

@corona10 corona10 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

@corona10
corona10 marked this pull request as draft October 4, 2026 03:09
@corona10

corona10 commented Oct 4, 2026 •

Copy link
Copy Markdown
Member Author

cc @x42005e1f @ZeroIntensity @picnixz

Here is the data (based on unittests, bm_nbody, bm_fannkuch), why I choose this way.
Shows a similar distribution.

allocated shrink calls % of calls cumulative realloc to same capacity bytes / real realloc
<= 4 351,958 27.8% 27.8% 351,935 (100%) 0 B
5 - 8 718,926 56.9% 84.7% 290,077 (40%) 38 B
9 - 16 158,291 12.5% 97.2% 0 45 B
17 - 32 30,160 2.4% 99.6% 0 121 B
33 - 64 4,031 0.3% 99.9% 0 258 B
65 - 128 646 0.05% 100.0% 0 404 B
> 128 501 0.04% 0 1.2 KB - 267 KB
total 1,264,513 642,012 (51%)

And here is the microbenchmark

Free-threading

main vs this PR

Benchmark main this PR
append+del[-1] size=0 14.8 ns 16.4 ns: 1.11x slower
append+pop size=0 20.7 ns 22.4 ns: 1.08x slower
append+del[-1] size=1 14.9 ns 16.4 ns: 1.10x slower
append+pop size=1 19.2 ns 13.7 ns: 1.40x faster
append+del[-1] size=3 15.0 ns 16.5 ns: 1.10x slower
append+pop size=3 18.3 ns 13.8 ns: 1.33x faster
append+del[-1] size=7 15.1 ns 16.3 ns: 1.08x slower
append+pop size=7 13.3 ns 13.7 ns: 1.03x slower
append+del[-1] size=15 15.0 ns 16.5 ns: 1.10x slower
append+pop size=15 13.4 ns 13.7 ns: 1.02x slower
append+del[-1] size=100 15.2 ns 16.3 ns: 1.08x slower
append+pop size=100 13.4 ns 13.7 ns: 1.02x slower
extend+del[S:] S=1 45.9 ns 40.4 ns: 1.14x faster
extend+del[S:] S=2 45.7 ns 41.0 ns: 1.11x faster
extend+del[S:] S=4 41.9 ns 43.3 ns: 1.03x slower
extend+del[S:] S=16 51.6 ns 52.3 ns: 1.01x slower
extend+del[S:] S=32 62.1 ns 63.5 ns: 1.02x slower
extend+del[S:] S=64 84.6 ns 87.5 ns: 1.03x slower
extend+del[S:] S=256 233 ns 243 ns: 1.04x slower
copy only N=32 69.3 ns 71.2 ns: 1.03x slower
del[-1] x N N=32 465 ns 573 ns: 1.23x slower
copy only N=64 90.6 ns 92.9 ns: 1.03x slower
del[-1] x N N=64 863 ns 1.04 us: 1.21x slower
copy only N=256 228 ns 232 ns: 1.02x slower
del[-1] x N N=256 3.29 us 3.71 us: 1.13x slower
del[-1] x N N=1024 12.9 us 14.2 us: 1.10x slower
copy only N=4096 6.12 us 6.23 us: 1.02x slower
del[-1] x N N=4096 70.6 us 77.6 us: 1.10x slower
copy only N=65536 108 us 111 us: 1.04x slower
del[-1] x N N=65536 1.18 ms 1.31 ms: 1.10x slower
del[0] x N N=256 12.3 us 12.9 us: 1.05x slower
del[0] x N N=4096 2.28 ms 2.31 ms: 1.02x slower
Geometric mean (ref) 1.03x slower

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

Benchmark main #158602
append+del[-1] size=0 14.8 ns 31.0 ns: 2.09x slower
append+pop size=0 20.7 ns 22.0 ns: 1.06x slower
append+del[-1] size=1 14.9 ns 23.1 ns: 1.55x slower
append+pop size=1 19.2 ns 20.5 ns: 1.07x slower
append+del[-1] size=3 15.0 ns 21.7 ns: 1.44x slower
append+pop size=3 18.3 ns 19.2 ns: 1.05x slower
append+del[-1] size=7 15.1 ns 16.1 ns: 1.07x slower
append+pop size=7 13.3 ns 13.6 ns: 1.02x slower
append+del[-1] size=15 15.0 ns 16.3 ns: 1.08x slower
append+del[-1] size=100 15.2 ns 16.1 ns: 1.06x slower
append+pop size=100 13.4 ns 13.6 ns: 1.02x slower
extend+del[S:] S=1 45.9 ns 47.4 ns: 1.03x slower
extend+del[S:] S=2 45.7 ns 46.5 ns: 1.02x slower
extend+del[S:] S=4 41.9 ns 43.0 ns: 1.03x slower
extend+del[S:] S=16 51.6 ns 52.6 ns: 1.02x slower
extend+del[S:] S=64 84.6 ns 88.7 ns: 1.05x slower
extend+del[S:] S=256 233 ns 240 ns: 1.03x slower
del[-1] x N N=32 465 ns 559 ns: 1.20x slower
del[-1] x N N=64 863 ns 1.01 us: 1.17x slower
copy only N=256 228 ns 233 ns: 1.02x slower
del[-1] x N N=256 3.29 us 3.64 us: 1.11x slower
del[-1] x N N=1024 12.9 us 14.0 us: 1.09x slower
copy only N=4096 6.12 us 6.23 us: 1.02x slower
del[-1] x N N=4096 70.6 us 75.3 us: 1.07x slower
copy only N=65536 108 us 110 us: 1.02x slower
del[-1] x N N=65536 1.18 ms 1.25 ms: 1.06x slower
del[0] x N N=256 12.3 us 12.8 us: 1.04x slower
Geometric mean (ref) 1.08x slower

Default build

main vs this PR

Benchmark main this PR
append+del[-1] size=0 12.8 ns 13.9 ns: 1.08x slower
append+pop size=0 15.5 ns 16.7 ns: 1.08x slower
append+del[-1] size=1 12.7 ns 13.8 ns: 1.09x slower
append+pop size=1 12.6 ns 11.4 ns: 1.10x faster
append+del[-1] size=3 13.0 ns 13.9 ns: 1.07x slower
append+pop size=3 12.8 ns 11.4 ns: 1.13x faster
append+del[-1] size=7 13.1 ns 13.6 ns: 1.04x slower
append+pop size=7 10.7 ns 11.2 ns: 1.05x slower
append+del[-1] size=15 13.1 ns 13.5 ns: 1.03x slower
append+pop size=15 10.8 ns 11.2 ns: 1.04x slower
append+del[-1] size=100 13.1 ns 13.6 ns: 1.04x slower
append+pop size=100 10.9 ns 11.2 ns: 1.03x slower
extend+del[S:] S=1 34.3 ns 33.8 ns: 1.02x faster
extend+del[S:] S=2 35.7 ns 34.6 ns: 1.03x faster
extend+del[S:] S=16 45.5 ns 44.9 ns: 1.01x faster
extend+del[S:] S=4096 2.91 us 2.75 us: 1.06x faster
copy only N=32 62.0 ns 62.8 ns: 1.01x slower
del[-1] x N N=32 411 ns 489 ns: 1.19x slower
del[-1] x N N=64 744 ns 872 ns: 1.17x slower
copy only N=256 209 ns 206 ns: 1.01x faster
del[-1] x N N=256 2.82 us 3.14 us: 1.11x slower
del[-1] x N N=1024 10.9 us 11.7 us: 1.08x slower
del[-1] x N N=4096 57.5 us 60.1 us: 1.05x slower
copy only N=65536 68.8 us 68.3 us: 1.01x faster
del[-1] x N N=65536 983 us 1.02 ms: 1.04x slower
del[0] x N N=256 4.27 us 4.85 us: 1.14x slower
del[0] x N N=4096 616 us 627 us: 1.02x slower
Geometric mean (ref) 1.03x slower

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 main #158602
append+del[-1] size=0 12.8 ns 64.1 ns: 5.01x slower
append+pop size=0 15.5 ns 16.0 ns: 1.03x slower
append+del[-1] size=1 12.7 ns 15.3 ns: 1.20x slower
append+del[-1] size=3 13.0 ns 15.5 ns: 1.19x slower
append+del[-1] size=7 13.1 ns 13.4 ns: 1.02x slower
append+pop size=7 10.7 ns 10.9 ns: 1.02x slower
append+del[-1] size=100 13.1 ns 13.3 ns: 1.02x slower
extend+del[S:] S=1 34.3 ns 33.4 ns: 1.03x faster
extend+del[S:] S=2 35.7 ns 34.8 ns: 1.03x faster
extend+del[S:] S=8 46.7 ns 46.1 ns: 1.01x faster
extend+del[S:] S=32 55.0 ns 53.6 ns: 1.03x faster
del[-1] x N N=32 411 ns 466 ns: 1.13x slower
del[-1] x N N=64 744 ns 853 ns: 1.15x slower
del[-1] x N N=256 2.82 us 3.18 us: 1.13x slower
del[-1] x N N=1024 10.9 us 11.5 us: 1.06x slower
del[-1] x N N=4096 57.5 us 60.4 us: 1.05x slower
del[-1] x N N=65536 983 us 1.01 ms: 1.02x slower
del[0] x N N=256 4.27 us 4.85 us: 1.13x slower
del[0] x N N=4096 616 us 627 us: 1.02x slower
Geometric mean (ref) 1.07x slower

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:

Script

import 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 list.pop() benefits as well
(1.33x-1.40x faster on small lists), since it already goes through
list_resize().

@corona10
corona10 marked this pull request as ready for review October 4, 2026 03:28
Comment thread Objects/listobject.c
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

@x42005e1f x42005e1f Oct 4, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@corona10 corona10 Oct 4, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better communication for this kind of issue is that

  1. 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
  2. Request to merge this PR after your PR is merged.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me, the second option seems preferable, so I am requesting it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In anycase, we need to wait until other core devs like this approach. So please be patient.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@picnixz

picnixz commented Oct 4, 2026

Copy link
Copy Markdown
Member

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 del[-1] x N N=32 411 ns 489 ns: 1.19x slower (default build) or del[-1] x N N=32 465 ns 573 ns: 1.23x slower (FT build). Ideally I would push small lists at N=64 to del[-1] x N N=64 863 ns 1.04 us: 1.21x slower (FT build) to mitigate this case as well (ISTM that at N=64 the performance loss is less pronounced than at N<=64 but having 64 as a "small" list may bee too much).

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).

@x42005e1f

x42005e1f commented Oct 4, 2026 •

Copy link
Copy Markdown

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 new_allocated == allocated, your measurements would have been quite different.

@StanFromIreland

Copy link
Copy Markdown
Member

@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.

@corona10

corona10 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

@picnixz

How many bytes exactly are we allocating for a list of size N?

IIUC, allocated * 8.

but if we have N=32, we could mitigate more cases such as del[-1] x N N=32 411 ns 489 ns: 1.19x slower (default build) or del[-1] x N N=32 465 ns 573 ns: 1.23x slower (FT build).

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.
But I am open to increasing this value to 32 since we can cover 99.6%.

Do you want to increase this to 32?

@corona10

corona10 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

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 new_allocated == allocated, your measurements would have been quite different.

I think everything is a trade-off here, we would lose too much performance to be frugal with very tiny lists.

@corona10 corona10 self-assigned this Oct 4, 2026
@x42005e1f

x42005e1f commented Oct 4, 2026 •

Copy link
Copy Markdown

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, new_allocated is as follows:

new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
/* Do not overallocate if the new size is closer to overallocated size
* than to the old size.
*/
if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))
new_allocated = ((size_t)newsize + 3) & ~(size_t)3;
if (newsize == 0)
new_allocated = 0;

Modeling for newsize ranging from 0 to 16:

>>> 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 -> 20

If we skip the reallocation for new_allocated == allocated, we can avoid expensive calls to the allocator. It is just an extra check before list_allocate_array(new_allocated).


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 newsize == 0 can be directly changed to new_allocated = 4 if you do not want to reset the capacity to zero; however, it can simply be deleted, since 6 & ~3 == 4).

@corona10

corona10 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

@x42005e1f

My "trade-off" comment was about calling list_resize() on every del without any tuning,
the performance we lose on very tiny lists is too much for the memory we get back.

@corona10

corona10 commented Oct 4, 2026

Copy link
Copy Markdown
Member Author

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 newsize == 0 can be directly changed to new_allocated = 4 if you do not want to reset the capacity to zero; however, it can simply be deleted, since 6 & ~3 == 4).

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.

@ZeroIntensity

Copy link
Copy Markdown
Member

I'm much more comfortable with this fix. 3-7% slower is a lot easier to swallow than the 160% from the other PR.

Comment thread Objects/listobject.c Outdated
Comment thread Objects/listobject.c
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Objects/listobject.c Outdated
@corona10

corona10 commented Oct 5, 2026 •

Copy link
Copy Markdown
Member Author

@eendebakpt

With your suggestion in free-threaded build (expect to be same as default build), it becomes much better:

Benchmark main this PR
append+del[-1] size=0 15.1 ns 16.0 ns: 1.06x slower
append+pop size=0 21.2 ns 23.2 ns: 1.09x slower
append+del[-1] size=1 15.3 ns 16.0 ns: 1.04x slower
append+pop size=1 19.8 ns 13.1 ns: 1.51x faster
append+pop size=3 18.8 ns 13.1 ns: 1.43x faster
append+del[-1] size=7 15.5 ns 15.8 ns: 1.02x slower
append+pop size=7 13.5 ns 13.2 ns: 1.03x faster
append+pop size=15 13.6 ns 13.1 ns: 1.03x faster
append+pop size=100 13.6 ns 13.3 ns: 1.02x faster
extend+del[S:] S=1 47.9 ns 39.5 ns: 1.21x faster
extend+del[S:] S=2 46.7 ns 39.8 ns: 1.17x faster
extend+del[S:] S=4 42.8 ns 40.9 ns: 1.04x faster
extend+del[S:] S=8 64.7 ns 42.3 ns: 1.53x faster
extend+del[S:] S=16 52.8 ns 52.2 ns: 1.01x faster
extend+del[S:] S=64 86.6 ns 89.5 ns: 1.03x slower
extend+del[S:] S=256 240 ns 244 ns: 1.02x slower
extend+del[S:] S=4096 2.94 us 3.03 us: 1.03x slower
copy only N=32 69.7 ns 71.4 ns: 1.02x slower
del[-1] x N N=32 469 ns 482 ns: 1.03x slower
del[-1] x N N=64 875 ns 938 ns: 1.07x slower
copy only N=256 231 ns 238 ns: 1.03x slower
del[-1] x N N=256 3.36 us 3.50 us: 1.04x slower
del[-1] x N N=1024 12.9 us 13.7 us: 1.06x slower
copy only N=4096 6.15 us 6.38 us: 1.04x slower
del[-1] x N N=4096 71.3 us 74.3 us: 1.04x slower
copy only N=65536 108 us 115 us: 1.06x slower
del[-1] x N N=65536 1.21 ms 1.25 ms: 1.04x slower
del[0] x N N=256 12.5 us 13.1 us: 1.04x slower
Geometric mean (ref) 1.02x faster

Benchmark hidden because not significant (8): append+del[-1] size=3, append+del[-1] size=15, append+del[-1] size=100, extend+del[S:] S=32, extend+del[S:] S=1024, copy only N=64, copy only N=1024, del[0] x N N=4096

corona10 and others added 2 commits October 5, 2026 16:03
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
@corona10

corona10 commented Oct 5, 2026

Copy link
Copy Markdown
Member Author

@ZeroIntensity @eendebakpt @picnixz
Can I get the approval before merging this PR?

Comment thread Objects/listobject.c
Comment on lines +128 to +129
Py_NO_INLINE static int
py_list_resize(PyListObject *self, Py_ssize_t newsize)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment explaining why we're forcing this to not be inlined?

Comment thread Objects/listobject.c
* than ob_size on entry.
*/
static int
static inline Py_ALWAYS_INLINE int

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And why is this one Py_ALWAYS_INLINE?

@@ -0,0 +1,2 @@
Skip the array reallocation when shrinking a small :class:`list`. Patch by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But large arrays are re-allocated! This PR solves a bug (feature?), which should be in the news entry.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants