Skip to content

Commit

Permalink
bpo-31179: Make dict.copy() up to 5.5 times faster. (python#3067)
Browse files Browse the repository at this point in the history
  • Loading branch information
1st1 committed Jan 22, 2018
1 parent a4b1bb4 commit b0a7a03
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 2 deletions.
50 changes: 48 additions & 2 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,11 +271,57 @@ def __new__(cls):
self.assertEqual(baddict3.fromkeys({"a", "b", "c"}), res)

def test_copy(self):
d = {1:1, 2:2, 3:3}
self.assertEqual(d.copy(), {1:1, 2:2, 3:3})
d = {1: 1, 2: 2, 3: 3}
self.assertIsNot(d.copy(), d)
self.assertEqual(d.copy(), d)
self.assertEqual(d.copy(), {1: 1, 2: 2, 3: 3})

copy = d.copy()
d[4] = 4
self.assertNotEqual(copy, d)

self.assertEqual({}.copy(), {})
self.assertRaises(TypeError, d.copy, None)

def test_copy_fuzz(self):
for dict_size in [10, 100, 1000, 10000, 100000]:
dict_size = random.randrange(
dict_size // 2, dict_size + dict_size // 2)
with self.subTest(dict_size=dict_size):
d = {}
for i in range(dict_size):
d[i] = i

d2 = d.copy()
self.assertIsNot(d2, d)
self.assertEqual(d, d2)
d2['key'] = 'value'
self.assertNotEqual(d, d2)
self.assertEqual(len(d2), len(d) + 1)

def test_copy_maintains_tracking(self):
class A:
pass

key = A()

for d in ({}, {'a': 1}, {key: 'val'}):
d2 = d.copy()
self.assertEqual(gc.is_tracked(d), gc.is_tracked(d2))

def test_copy_noncompact(self):
# Dicts don't compact themselves on del/pop operations.
# Copy will use a slow merging strategy that produces
# a compacted copy when roughly 33% of dict is a non-used
# keys-space (to optimize memory footprint).
# In this test we want to hit the slow/compacting
# branch of dict.copy() and make sure it works OK.
d = {k: k for k in range(1000)}
for k in range(950):
del d[k]
d2 = d.copy()
self.assertEqual(d2, d)

def test_get(self):
d = {}
self.assertIs(d.get('c'), None)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make dict.copy() up to 5.5 times faster.
73 changes: 73 additions & 0 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,52 @@ new_dict_with_shared_keys(PyDictKeysObject *keys)
return new_dict(keys, values);
}


static PyObject *
clone_combined_dict(PyDictObject *orig)
{
assert(PyDict_CheckExact(orig));
assert(orig->ma_values == NULL);
assert(orig->ma_keys->dk_refcnt == 1);

Py_ssize_t keys_size = _PyDict_KeysSize(orig->ma_keys);
PyDictKeysObject *keys = PyObject_Malloc(keys_size);
if (keys == NULL) {
PyErr_NoMemory();
return NULL;
}

memcpy(keys, orig->ma_keys, keys_size);

/* After copying key/value pairs, we need to incref all
keys and values and they are about to be co-owned by a
new dict object. */
PyDictKeyEntry *ep0 = DK_ENTRIES(keys);
Py_ssize_t n = keys->dk_nentries;
for (Py_ssize_t i = 0; i < n; i++) {
PyDictKeyEntry *entry = &ep0[i];
PyObject *value = entry->me_value;
if (value != NULL) {
Py_INCREF(value);
Py_INCREF(entry->me_key);
}
}

PyDictObject *new = (PyDictObject *)new_dict(keys, NULL);
if (new == NULL) {
/* In case of an error, `new_dict()` takes care of
cleaning up `keys`. */
return NULL;
}
new->ma_used = orig->ma_used;
assert(_PyDict_CheckConsistency(new));
if (_PyObject_GC_IS_TRACKED(orig)) {
/* Maintain tracking. */
_PyObject_GC_TRACK(new);
}
return (PyObject *)new;
}

PyObject *
PyDict_New(void)
{
Expand Down Expand Up @@ -2484,7 +2530,13 @@ PyDict_Copy(PyObject *o)
PyErr_BadInternalCall();
return NULL;
}

mp = (PyDictObject *)o;
if (mp->ma_used == 0) {
/* The dict is empty; just return a new dict. */
return PyDict_New();
}

if (_PyDict_HasSplitTable(mp)) {
PyDictObject *split_copy;
Py_ssize_t size = USABLE_FRACTION(DK_SIZE(mp->ma_keys));
Expand All @@ -2510,6 +2562,27 @@ PyDict_Copy(PyObject *o)
_PyObject_GC_TRACK(split_copy);
return (PyObject *)split_copy;
}

if (PyDict_CheckExact(mp) && mp->ma_values == NULL &&
(mp->ma_used >= (mp->ma_keys->dk_nentries * 2) / 3))
{
/* Use fast-copy if:
(1) 'mp' is an instance of a subclassed dict; and
(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 1/3 non-used keys.
The last condition (3) is important to guard against a pathalogical
case when a large dict is almost emptied with multiple del/pop
operations and copied after that. In cases like this, we defer to
PyDict_Merge, which produces a compacted copy.
*/
return clone_combined_dict(mp);
}

copy = PyDict_New();
if (copy == NULL)
return NULL;
Expand Down

0 comments on commit b0a7a03

Please sign in to comment.