diff -r 2a4fb01fa1a3 Lib/test/test_dict_split.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Lib/test/test_dict_split.py Sat Sep 17 00:14:58 2016 +0900 @@ -0,0 +1,88 @@ +"""Tests for split table form of dict""" + +import struct +import sys +from test import support +import unittest + + +def combined_dict_size(keysize): + """size of combined dict""" + size = support.calcobjsize('n2P') # PyDictObject + size += struct.calcsize('2nPn') # PyDictKeysObject + size += struct.calcsize('n2P') * keysize # dk_entries + return size + + +def split_dict_size(keysize): + """size of split, but not shared dict""" + size = combined_dict_size(keysize) + size += struct.calcsize('P') * keysize # ma_values + return size + + +def shared_dict_size(keysize): + """size of shared dict""" + size = support.calcobjsize('n2P') # PyDictObject + size += struct.calcsize('P') * keysize # ma_values + return size + + +@support.cpython_only +class SplitTableTest(unittest.TestCase): + + check_sizeof = support.check_sizeof + + def make_shared_key_dict(self, n): + class C: + pass + + dicts = [] + for i in range(n): + a = C() + a.x, a.y, a.z = 1, 2, 3 + dicts.append(a.__dict__) + + return dicts + + def test_pop_pending(self): + """pop a pending key in a splitted table should not crash""" + a, b = self.make_shared_key_dict(2) + + a['a'] = 4 + with self.assertRaises(KeyError): + b.pop('a') + + def test_popitem(self): + """split table must be combined when d.popitem()""" + a, b = self.make_shared_key_dict(2) + + orig_size = sys.getsizeof(a) + + key = a.popitem()[0] # split table is combined + with self.assertRaises(KeyError): + del a[key] + + self.assertGreater(sys.getsizeof(a), orig_size) + + def test_setitem_after_pop(self): + class C: + pass + a = C() + + a.a = 1 + self.check_sizeof({}, combined_dict_size(8)) + self.check_sizeof(a.__dict__, shared_dict_size(4)) + + # dict.popitem() convert it to combined table + # C doesn't stop using shared key when bypass delitem. + a.__dict__.popitem() + self.check_sizeof(a.__dict__, combined_dict_size(8)) + + # But C should not convert a.__dict__ to split table again. + a.a = 1 + self.check_sizeof(a.__dict__, combined_dict_size(8)) + + +if __name__ == "__main__": + unittest.main() diff -r 2a4fb01fa1a3 Objects/dictobject.c --- a/Objects/dictobject.c Fri Sep 16 17:31:06 2016 +0300 +++ b/Objects/dictobject.c Sat Sep 17 00:14:58 2016 +0900 @@ -985,8 +985,10 @@ return NULL; } else if (mp->ma_keys->dk_lookup == lookdict_unicode) { - /* Remove dummy keys */ - if (dictresize(mp, DK_SIZE(mp->ma_keys))) + /* Remove dummy keys + * -1 is required since dictresize() uses key size > minused + */ + if (dictresize(mp, DK_SIZE(mp->ma_keys) - 1)) return NULL; } assert(mp->ma_keys->dk_lookup == lookdict_unicode_nodummy); @@ -2473,7 +2475,8 @@ } /* Convert split table to combined table */ if (mp->ma_keys->dk_lookup == lookdict_split) { - if (dictresize(mp, DK_SIZE(mp->ma_keys))) { + /* -1 is required since dictresize() uses key size > minused */ + if (dictresize(mp, DK_SIZE(mp->ma_keys) - 1)) { Py_DECREF(res); return NULL; } @@ -3848,9 +3851,11 @@ CACHED_KEYS(tp) = NULL; DK_DECREF(cached); } - } else { + } + else { + int shared = cached == ((PyDictObject *)dict)->ma_keys; res = PyDict_SetItem(dict, key, value); - if (cached != ((PyDictObject *)dict)->ma_keys) { + if (shared && cached != ((PyDictObject *)dict)->ma_keys) { /* Either update tp->ht_cached_keys or delete it */ if (cached->dk_refcnt == 1) { CACHED_KEYS(tp) = make_keys_shared(dict);