Skip to content

bpo-36229: Avoid unnecessary copies for list, set, and bytearray ops.#12226

Closed
brandtbucher wants to merge 5 commits intopython:masterfrom
brandtbucher:linear-collection-ops
Closed

bpo-36229: Avoid unnecessary copies for list, set, and bytearray ops.#12226
brandtbucher wants to merge 5 commits intopython:masterfrom
brandtbucher:linear-collection-ops

Conversation

@brandtbucher
Copy link
Copy Markdown
Member

@brandtbucher brandtbucher commented Mar 7, 2019

The relevant changes are all in the first commit, which has a cleaner diff. The second commit reorganized the affected functions to avoid separate declarations... which sort of mangled it. The additions are actually quite small (about 30 lines of code, total).

https://bugs.python.org/issue36229

If a list, set, or bytearray object's refcount is exactly one, binary operations delegate to their in-place counterparts, rather than creating expensive copies.
This removes the need for separate declarations of in-place functions.
This still tests that the list constructor efficiently allocates space...  it just might not be *exactly* the same size as the given example, as was implied before.
...feels pretty cool.
@rhettinger rhettinger self-assigned this Mar 8, 2019
@remilapeyre
Copy link
Copy Markdown

This will only work when there is no reference on the object like [0]*10 but it won't work when it is a variable like list2 = list1 + [0].

Except for creating test cases or large lists of constants, does this happen a lot?
Is there examples?

I would think most of the time the operands come from arguments or a local variable and this optimization would not be used, and most of the time list3 = list1 + list2 would still be quadratic.

Did you see on improvement for real use-cases?

@brandtbucher
Copy link
Copy Markdown
Member Author

brandtbucher commented Mar 8, 2019

@remilapeyre: beyond the first element, it's irrelevant whether the lists are bound to names or how many references they have. Currently, when adding list1 + list2 + list3, two copies (with one reference each) are made:

  • The intermediate result of list1 + list2.
  • The sum of this intermediate result and list3.

Only the second of these is actually returned - the first is used once and thrown away. The effect of this patch is to only create at most one copy for any arbitrarily long list summation, as this intermediate result will just mutate in-place for lists 3-n.

Anyone who's been disappointed by the quadratic complexity of sum(list_of_lists, []) will realize the value of this improvement. Indeed, there's even a discouraging comment in the sum source code:

/* It's tempting to use PyNumber_InPlaceAdd instead of
PyNumber_Add here, to avoid quadratic running time
when doing 'sum(list_of_lists, [])'.  However, this
would produce a change in behaviour: a snippet like

    empty = []
    sum([[x] for x in range(10)], empty)

would change the value of empty. */
temp = PyNumber_Add(result, item);

With the patch, list and bytearray get this sum improvement without the mentioned side-effect on empty.

@brandtbucher brandtbucher changed the title bpo-36229: Linear-time ops for some mutable collections. bpo-36229: Linear-time list, set, and bytearray ops. Mar 11, 2019
@remilapeyre
Copy link
Copy Markdown

@brandtbucher I get it now, thanks for explaining. This seems like a very nice trick.

Isn't the idiomatic way to do sum(l, []), list(chain.from_iterable(l))?

I made some measures and with your improvement sum gets significantly faster than chain:

>>> timeit.timeit("list(chain.from_iterable(l))", "from itertools import chain; l = [[i]*1000 for i in range(1000)]", number=100)
2.823833307999962
>>> timeit.timeit("sum(l, [])", "l = [[i]*1000 for i in range(1000)]", number=100)                                    2.0547430219999683

Without your change sum(l, []) takes a ridiculous amount of time. I did not check whether chain could be improved yet (I suppose so).

@brandtbucher brandtbucher changed the title bpo-36229: Linear-time list, set, and bytearray ops. bpo-36229: Avoid unnecessary copies for list, set, and bytearray ops. Mar 13, 2019
@rhettinger rhettinger closed this Mar 14, 2019
@brandtbucher brandtbucher deleted the linear-collection-ops branch March 21, 2019 03:55
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.

5 participants