Skip to content

Commit

Permalink
bpo-45340: Don't create object dictionaries unless actually needed (G…
Browse files Browse the repository at this point in the history
…H-28802)

* Never change types' cached keys. It could invalidate inline attribute objects.

* Lazily create object dictionaries.

* Update specialization of LOAD/STORE_ATTR.

* Don't update shared keys version for deletion of value.

* Update gdb support to handle instance values.

* Rename SPLIT_KEYS opcodes to INSTANCE_VALUE.
  • Loading branch information
markshannon authored Oct 13, 2021
1 parent 97308df commit a8b9350
Show file tree
Hide file tree
Showing 18 changed files with 721 additions and 400 deletions.
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

0 comments on commit a8b9350

Please sign in to comment.