Skip to content

bpo-45340: Don't create object dictionaries unless actually needed #28802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Oct 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8214006
Add insertion order bit-vector to dict values to allow dicts to share…
markshannon Sep 21, 2021
45c9814
Add NEWS
markshannon Sep 23, 2021
c3de09e
Never change types' cached keys. It could invalidate inline attribute…
markshannon Sep 23, 2021
58dd2a0
Lazily create object dictionaries. Work in progress.
markshannon Sep 23, 2021
769183a
Restore specialization for inline attributes.
markshannon Sep 30, 2021
1e6cd1f
Turn on stats
markshannon Sep 30, 2021
a8fbb34
Fix specialization of LOAD/STORE_ATTR.
markshannon Oct 1, 2021
37d8cc6
Turn off stats.
markshannon Oct 1, 2021
0e022f9
Don't update shared keys version for deletion of value.
markshannon Oct 4, 2021
ddd2e2c
Merge main into inline-attributes-with-opt
markshannon Oct 6, 2021
e979575
Fix refrence leak.
markshannon Oct 6, 2021
42361ad
Change 'InlineAttribute' to 'InstanceAttribute' as they are not inlin…
markshannon Oct 6, 2021
e9b09d5
Merge branch 'main' into inline-attributes-with-opt
markshannon Oct 6, 2021
70dc34f
Update gdb support to handle instance values.
markshannon Oct 7, 2021
022f2c5
Rename SPLIT_KEYS opcodes to INSTANCE_VALUE.
markshannon Oct 7, 2021
4ec5f11
Tidy test_gc
markshannon Oct 7, 2021
1818a5a
Don't lose errors.
markshannon Oct 7, 2021
bfc7be0
Make sure order is updated when deleting from values array.
markshannon Oct 7, 2021
62a3a59
Update test_dict.
markshannon Oct 7, 2021
7978220
Add NEWS
markshannon Oct 8, 2021
b25b1d9
Merge branch 'main' into inline-attributes-with-opt
markshannon Oct 12, 2021
7b2efa4
Merge branch 'main' into inline-attributes-with-opt
markshannon Oct 12, 2021
459e29e
Make sure that instance dicts are GC tracked when necessary.
markshannon Oct 12, 2021
83b90c5
Support older versions of Python in gdb.
markshannon Oct 12, 2021
8c894f3
Rename function and move to internal header.
markshannon Oct 12, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ struct _typeobject {

destructor tp_finalize;
vectorcallfunc tp_vectorcall;
Py_ssize_t tp_inline_values_offset;
};

/* The *real* layout of a type object when allocated on the heap */
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ extern uint64_t _pydict_global_version;

#define DICT_NEXT_VERSION() (++_pydict_global_version)

PyObject *_PyObject_MakeDictFromInstanceAttributes(PyObject *obj, PyDictValues *values);

#ifdef __cplusplus
}
#endif
Expand Down
10 changes: 10 additions & 0 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ extern int _Py_CheckSlotResult(
extern PyObject* _PyType_AllocNoTrack(PyTypeObject *type, Py_ssize_t nitems);

extern int _PyObject_InitializeDict(PyObject *obj);
extern int _PyObject_StoreInstanceAttribute(PyObject *obj, PyDictValues *values,
PyObject *name, PyObject *value);
PyObject * _PyObject_GetInstanceAttribute(PyObject *obj, PyDictValues *values,
PyObject *name);
PyDictValues ** _PyObject_ValuesPointer(PyObject *);
PyObject ** _PyObject_DictPointer(PyObject *);
int _PyObject_VisitInstanceAttributes(PyObject *self, visitproc visit, void *arg);
void _PyObject_ClearInstanceAttributes(PyObject *self);
void _PyObject_FreeInstanceAttributes(PyObject *self);
int _PyObject_IsInstanceDictEmpty(PyObject *);

#ifdef __cplusplus
}
Expand Down
1 change: 1 addition & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,7 @@ given type object has a specified feature.
*/

#ifndef Py_LIMITED_API

/* Set if instances of the type object are treated as sequences for pattern matching */
#define Py_TPFLAGS_SEQUENCE (1 << 5)
/* Set if instances of the type object are treated as mappings for pattern matching */
Expand Down
21 changes: 11 additions & 10 deletions Include/opcode.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions Lib/opcode.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ def jabs_op(name, op):
"BINARY_SUBSCR_DICT",
"JUMP_ABSOLUTE_QUICK",
"LOAD_ATTR_ADAPTIVE",
"LOAD_ATTR_SPLIT_KEYS",
"LOAD_ATTR_INSTANCE_VALUE",
"LOAD_ATTR_WITH_HINT",
"LOAD_ATTR_SLOT",
"LOAD_ATTR_MODULE",
Expand All @@ -242,8 +242,9 @@ def jabs_op(name, op):
"LOAD_METHOD_CACHED",
"LOAD_METHOD_CLASS",
"LOAD_METHOD_MODULE",
"LOAD_METHOD_NO_DICT",
"STORE_ATTR_ADAPTIVE",
"STORE_ATTR_SPLIT_KEYS",
"STORE_ATTR_INSTANCE_VALUE",
"STORE_ATTR_SLOT",
"STORE_ATTR_WITH_HINT",
# Super instructions
Expand Down
10 changes: 6 additions & 4 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -5500,17 +5500,19 @@ class A:
class B(A):
pass

#Shrink keys by repeatedly creating instances
[(A(), B()) for _ in range(20)]

a, b = A(), B()
self.assertEqual(sys.getsizeof(vars(a)), sys.getsizeof(vars(b)))
self.assertLess(sys.getsizeof(vars(a)), sys.getsizeof({"a":1}))
# Initial hash table can contain at most 5 elements.
# Initial hash table can contain only one or two elements.
# Set 6 attributes to cause internal resizing.
a.x, a.y, a.z, a.w, a.v, a.u = range(6)
self.assertNotEqual(sys.getsizeof(vars(a)), sys.getsizeof(vars(b)))
a2 = A()
self.assertEqual(sys.getsizeof(vars(a)), sys.getsizeof(vars(a2)))
self.assertLess(sys.getsizeof(vars(a)), sys.getsizeof({"a":1}))
b.u, b.v, b.w, b.t, b.s, b.r = range(6)
self.assertGreater(sys.getsizeof(vars(a)), sys.getsizeof(vars(a2)))
self.assertLess(sys.getsizeof(vars(a2)), sys.getsizeof({"a":1}))
self.assertLess(sys.getsizeof(vars(b)), sys.getsizeof({"a":1}))


Expand Down
5 changes: 2 additions & 3 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -994,8 +994,8 @@ class C:

@support.cpython_only
def test_splittable_setdefault(self):
"""split table must be combined when setdefault()
breaks insertion order"""
"""split table must keep correct insertion
order when attributes are adding using setdefault()"""
a, b = self.make_shared_key_dict(2)

a['a'] = 1
Expand All @@ -1005,7 +1005,6 @@ def test_splittable_setdefault(self):
size_b = sys.getsizeof(b)
b['a'] = 1

self.assertGreater(size_b, size_a)
self.assertEqual(list(a), ['x', 'y', 'z', 'a', 'b'])
self.assertEqual(list(b), ['x', 'y', 'z', 'b', 'a'])

Expand Down
29 changes: 14 additions & 15 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ def __getattr__(self, someattribute):
# 0, thus mutating the trash graph as a side effect of merely asking
# whether __del__ exists. This used to (before 2.3b1) crash Python.
# Now __getattr__ isn't called.
self.assertEqual(gc.collect(), 4)
self.assertEqual(gc.collect(), 2)
self.assertEqual(len(gc.garbage), garbagelen)

def test_boom2(self):
Expand All @@ -471,7 +471,7 @@ def __getattr__(self, someattribute):
# there isn't a second time, so this simply cleans up the trash cycle.
# We expect a, b, a.__dict__ and b.__dict__ (4 objects) to get
# reclaimed this way.
self.assertEqual(gc.collect(), 4)
self.assertEqual(gc.collect(), 2)
self.assertEqual(len(gc.garbage), garbagelen)

def test_boom_new(self):
Expand All @@ -491,7 +491,7 @@ def __getattr__(self, someattribute):
gc.collect()
garbagelen = len(gc.garbage)
del a, b
self.assertEqual(gc.collect(), 4)
self.assertEqual(gc.collect(), 2)
self.assertEqual(len(gc.garbage), garbagelen)

def test_boom2_new(self):
Expand All @@ -513,7 +513,7 @@ def __getattr__(self, someattribute):
gc.collect()
garbagelen = len(gc.garbage)
del a, b
self.assertEqual(gc.collect(), 4)
self.assertEqual(gc.collect(), 2)
self.assertEqual(len(gc.garbage), garbagelen)

def test_get_referents(self):
Expand Down Expand Up @@ -943,8 +943,8 @@ def getstats():
A()
t = gc.collect()
c, nc = getstats()
self.assertEqual(t, 2*N) # instance object & its dict
self.assertEqual(c - oldc, 2*N)
self.assertEqual(t, N) # instance objects
self.assertEqual(c - oldc, N)
self.assertEqual(nc - oldnc, 0)

# But Z() is not actually collected.
Expand All @@ -964,8 +964,8 @@ def getstats():
Z()
t = gc.collect()
c, nc = getstats()
self.assertEqual(t, 2*N)
self.assertEqual(c - oldc, 2*N)
self.assertEqual(t, N)
self.assertEqual(c - oldc, N)
self.assertEqual(nc - oldnc, 0)

# The A() trash should have been reclaimed already but the
Expand All @@ -974,8 +974,8 @@ def getstats():
zs.clear()
t = gc.collect()
c, nc = getstats()
self.assertEqual(t, 4)
self.assertEqual(c - oldc, 4)
self.assertEqual(t, 2)
self.assertEqual(c - oldc, 2)
self.assertEqual(nc - oldnc, 0)

gc.enable()
Expand Down Expand Up @@ -1128,8 +1128,7 @@ def test_collect_generation(self):
@cpython_only
def test_collect_garbage(self):
self.preclean()
# Each of these cause four objects to be garbage: Two
# Uncollectables and their instance dicts.
# Each of these cause two objects to be garbage:
Uncollectable()
Uncollectable()
C1055820(666)
Expand All @@ -1138,8 +1137,8 @@ def test_collect_garbage(self):
if v[1] != "stop":
continue
info = v[2]
self.assertEqual(info["collected"], 2)
self.assertEqual(info["uncollectable"], 8)
self.assertEqual(info["collected"], 1)
self.assertEqual(info["uncollectable"], 4)

# We should now have the Uncollectables in gc.garbage
self.assertEqual(len(gc.garbage), 4)
Expand All @@ -1156,7 +1155,7 @@ def test_collect_garbage(self):
continue
info = v[2]
self.assertEqual(info["collected"], 0)
self.assertEqual(info["uncollectable"], 4)
self.assertEqual(info["uncollectable"], 2)

# Uncollectables should be gone
self.assertEqual(len(gc.garbage), 0)
Expand Down
10 changes: 5 additions & 5 deletions Lib/test/test_sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -1409,7 +1409,7 @@ def delx(self): del self.__x
check((1,2,3), vsize('') + 3*self.P)
# type
# static type: PyTypeObject
fmt = 'P2nPI13Pl4Pn9Pn11PIPP'
fmt = 'P2nPI13Pl4Pn9Pn12PIPP'
s = vsize(fmt)
check(int, s)
# class
Expand All @@ -1422,15 +1422,15 @@ def delx(self): del self.__x
'5P')
class newstyleclass(object): pass
# Separate block for PyDictKeysObject with 8 keys and 5 entries
check(newstyleclass, s + calcsize(DICT_KEY_STRUCT_FORMAT) + 8 + 5*calcsize("n2P"))
check(newstyleclass, s + calcsize(DICT_KEY_STRUCT_FORMAT) + 32 + 21*calcsize("n2P"))
# dict with shared keys
check(newstyleclass().__dict__, size('nQ2P') + 5*self.P)
check(newstyleclass().__dict__, size('nQ2P') + 15*self.P)
o = newstyleclass()
o.a = o.b = o.c = o.d = o.e = o.f = o.g = o.h = 1
# Separate block for PyDictKeysObject with 16 keys and 10 entries
check(newstyleclass, s + calcsize(DICT_KEY_STRUCT_FORMAT) + 16 + 10*calcsize("n2P"))
check(newstyleclass, s + calcsize(DICT_KEY_STRUCT_FORMAT) + 32 + 21*calcsize("n2P"))
# dict with shared keys
check(newstyleclass().__dict__, size('nQ2P') + 10*self.P)
check(newstyleclass().__dict__, size('nQ2P') + 13*self.P)
# unicode
# each tuple contains a string and its expected character size
# don't put any static strings here, as they may contain
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Object attributes are held in an array instead of a dictionary. An object's
dictionary are created lazily, only when needed. Reduces the memory
consumption of a typical Python object by about 30%. Patch by Mark Shannon.
Loading