Skip to content

bpo-31179: Make dict.copy() up to 5.5 times faster.#3067

Merged
1st1 merged 5 commits into
python:masterfrom
1st1:dictcopy
Jan 22, 2018
Merged

bpo-31179: Make dict.copy() up to 5.5 times faster.#3067
1st1 merged 5 commits into
python:masterfrom
1st1:dictcopy

Conversation

@1st1

@1st1 1st1 commented Aug 10, 2017

Copy link
Copy Markdown
Member

@1st1
1st1 requested a review from methane August 10, 2017 21:35
@vstinner vstinner added the performance Performance or resource usage label Aug 10, 2017

@methane methane left a comment

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.

I commented some personal preferences.

Comment thread Objects/dictobject.c Outdated

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.

I prefer old, orig, or source to ms.
And I prefer "declare and initialize at same time" style because we can use C99 syntax.

Comment thread Objects/dictobject.c Outdated

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.

I prefer "early return" style without "else" clause.

@1st1

1st1 commented Aug 11, 2017

Copy link
Copy Markdown
Member Author

@methane Thanks for the review! Please take a another look, I think I've addressed all of your comments.

Comment thread Objects/dictobject.c Outdated

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.

I meant all local variables can be defined where it's initialized.
for example,

for (Py_ssize_t i = 0; i < keys->dk_nentries; i++) {
    PyDictKeyEntry *entry = &ep0[i];
...

https://google.github.io/styleguide/cppguide.html#Local_Variables

C++ allows you to declare variables anywhere in a function. We encourage you to declare them in as local a scope as possible, and as close to the first use as possible. This makes it easier for the reader to find the declaration and see what type the variable is and what it was initialized to.

C99 is same to C++ in this case.

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.

Done

@methane methane left a comment

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.

LGTM except my personal preference coding style.

Comment thread Lib/test/test_dict.py Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Are these test changes actually related to the code changes? Or are they just test hardening that you thought of because of the new code? If so, the test improvements should go in regardless of whether or not the code changes do, and ideally as a separate commit.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The one case it slowed down was an empty dict ... is it worth a special path to drop out and just create a new empty dict then?

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.

Added a fast-path for empty dicts.

Comment thread Objects/dictobject.c Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

incref (missing the 'c')

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.

fixed

Comment thread Objects/dictobject.c Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Am I missing some context, or is this refusing to use the fast path for shared-key dicts, when the fast path would still be good for 2/3 arrays, and the other just replaced by an INCREF?

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.

I don't want to complicate the code for speeding up shared-key dicts copy. Copying a shared dict is a very rare operation.

Comment thread Objects/dictobject.c Outdated

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Any reason not to at least use presized?

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.

This change isn't concerned with updating/touching the slow path.

Comment thread Objects/dictobject.c Outdated


static PyObject *
clone_dict(PyDictObject *orig)

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.

would you mind to rename the function to "clone_combined_dict()"? It requires ma_values==NULL.

Comment thread Objects/dictobject.c Outdated
(2) 'mp' is not a split-dict; and

(3) if 'mp' is non-compact ('del' operation does not resize dicts),
do fast-copy only if it has at most 3 non-used keys.

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.

Instead of an absolute number, why not using a ratio? 3 unused is may a lot if the dict contains 16 items, but what is the dict contains 2^16 items?

Would it make sense to use a ratio, tolerate a maximum of 10% unused entries? Or 1%?

What does impact performances of dict.set(): the absolute number or the ratio?

If it's the absolute number, just explain that above, something like: "each unused entry make dict.set() slower, so create a compacted dict for optimal performances".

Comment thread Objects/dictobject.c Outdated
assert(orig->ma_keys->dk_refcnt == 1);

Py_ssize_t keys_size = _PyDict_KeysSize(orig->ma_keys);
PyDictKeysObject *keys = PyObject_MALLOC(keys_size);

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.

MALLOC is an an alias, use directly PyObject_Malloc().

Comment thread Objects/dictobject.c Outdated
}

if (PyDict_CheckExact(mp) &&
mp->ma_values == NULL && mp->ma_keys->dk_refcnt == 1 &&

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.

dk_refcnt==1 test seems redundant, the inner assertion is enough IMHO.

Extract of dictobject.c:

The DictObject can be in one of two forms.

Either:
  A combined table:
    ma_values == NULL, dk_refcnt == 1.
    Values are stored in the me_value field of the PyDictKeysObject.
Or:
  A split table:
    ma_values != NULL, dk_refcnt >= 1
    Values are stored in the ma_values array.
    Only string (unicode) keys are allowed.
    All dicts sharing same key must have same insertion order.

dk_refcnt can only be != 1 if ma_values is not NULL.

@bedevere-bot

Copy link
Copy Markdown

When you're done making the requested changes, leave the comment: I have made the requested changes; please review again.

@vstinner

Copy link
Copy Markdown
Member

The overall change LGTM, but I have a few questions and small change requests.

@1st1

1st1 commented Jan 22, 2018

Copy link
Copy Markdown
Member Author

I have made the requested changes; please review again.

@bedevere-bot

Copy link
Copy Markdown

Thanks for making the requested changes!

@vstinner, @methane: please review the changes made to this pull request.

@1st1

1st1 commented Jan 22, 2018

Copy link
Copy Markdown
Member Author

@vstinner I've addressed your comments; I agree using a ratio looks like a more sensible approach. I've set it to 20%. The number is pretty arbitrary but the good news we can always adjust it.

@1st1

1st1 commented Jan 22, 2018

Copy link
Copy Markdown
Member Author

@vstinner Pushed another update; now integer arithmetic is used to compute the ratio as you suggested.

@vstinner

Copy link
Copy Markdown
Member

Thanks for the updates @1st1, the new change now LGTM.

@1st1
1st1 merged commit b0a7a03 into python:master Jan 22, 2018
@1st1
1st1 deleted the dictcopy branch January 22, 2018 16:54
@1st1

1st1 commented Jan 22, 2018

Copy link
Copy Markdown
Member Author

Thanks @methane and @vstinner!

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

Labels

performance Performance or resource usage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants