Skip to content

Commit 4a9f0fa

Browse files
eendebakptclaude
andcommitted
Use a managed dict for the _io types
Replace the explicit dict slot of _io._IOBase and its C subclasses with Py_TPFLAGS_MANAGED_DICT, so instance attributes are stored as inline values with shared keys instead of a per-instance dict, and attribute access can be specialized. The flag is inherited by all C and Python subclasses. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 86be79a commit 4a9f0fa

9 files changed

Lines changed: 96 additions & 75 deletions

File tree

‎Lib/test/test_io/test_general.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,41 @@ def test_immutable_types(self):
895895
with self.assertRaisesRegex(TypeError, "immutable"):
896896
tp.foo = "bar"
897897

898+
def test_managed_dict(self):
899+
# The _io types use Py_TPFLAGS_MANAGED_DICT: there is no explicit
900+
# dict slot and instance attributes are stored as inline values.
901+
_testinternalcapi = import_helper.import_module("_testinternalcapi")
902+
_io = self._io
903+
no_dict = {_io.IncrementalNewlineDecoder, _io._BytesIOBuffer}
904+
for tp in self.types:
905+
with self.subTest(tp=tp):
906+
if tp in no_dict:
907+
self.assertEqual(tp.__dictoffset__, 0)
908+
else:
909+
self.assertEqual(tp.__dictoffset__, -1)
910+
911+
# Use fresh subclasses: each class has its own shared keys, which
912+
# may already be exhausted on the base types by earlier tests.
913+
class BytesIO(_io.BytesIO): pass
914+
class StringIO(_io.StringIO): pass
915+
class BufferedReader(_io.BufferedReader): pass
916+
class TextIOWrapper(_io.TextIOWrapper): pass
917+
class RawIO(_io._RawIOBase):
918+
def __init__(self):
919+
self.attr = 1
920+
instances = [
921+
BytesIO(b"data"),
922+
StringIO("data"),
923+
BufferedReader(_io.BytesIO(b"data")),
924+
TextIOWrapper(_io.BytesIO()),
925+
RawIO(),
926+
]
927+
for obj in instances:
928+
with self.subTest(obj=type(obj).__name__):
929+
obj.attr = 1
930+
self.assertTrue(_testinternalcapi.has_inline_values(obj))
931+
self.assertEqual(obj.__dict__["attr"], 1)
932+
898933
def test_class_hierarchy(self):
899934
def check_subs(types, base):
900935
for tp in types:

‎Lib/test/test_io/test_memoryio.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io
1111
import _pyio as pyio
1212
import pickle
13+
import struct
1314
import sys
1415
import weakref
1516

@@ -1037,11 +1038,13 @@ def test_setstate(self):
10371038
@support.cpython_only
10381039
def test_sizeof(self):
10391040
if support.Py_GIL_DISABLED:
1040-
basesize = support.calcobjsize('P2n2Pni')
1041+
basesize = support.calcobjsize('P2nPni')
10411042
else:
1042-
basesize = support.calcobjsize('P2n2Pn')
1043+
basesize = support.calcobjsize('P2nPn')
10431044
check = self.check_sizeof
10441045
self.assertEqual(object.__sizeof__(io.BytesIO()), basesize)
1046+
# sys.getsizeof() includes the managed dict pre-header
1047+
basesize += 2 * struct.calcsize('P')
10451048
check(io.BytesIO(), basesize )
10461049
n = 1000 # use a variable to prevent constant folding
10471050
check(io.BytesIO(b'a' * n), basesize + sys.getsizeof(b'a' * n))

‎Modules/_io/bufferedio.c‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,6 @@ typedef struct {
260260
Py_ssize_t buffer_size;
261261
Py_ssize_t buffer_mask;
262262

263-
PyObject *dict;
264263
PyObject *weakreflist;
265264
} buffered;
266265

@@ -417,7 +416,7 @@ buffered_clear(PyObject *op)
417416
buffered *self = buffered_CAST(op);
418417
self->ok = 0;
419418
Py_CLEAR(self->raw);
420-
Py_CLEAR(self->dict);
419+
PyObject_ClearManagedDict(op);
421420
return 0;
422421
}
423422

@@ -467,8 +466,7 @@ buffered_traverse(PyObject *op, visitproc visit, void *arg)
467466
buffered *self = buffered_CAST(op);
468467
Py_VISIT(Py_TYPE(self));
469468
Py_VISIT(self->raw);
470-
Py_VISIT(self->dict);
471-
return 0;
469+
return PyObject_VisitManagedDict(op, visit, arg);
472470
}
473471

474472
/* Because this can call arbitrary code, it shouldn't be called when
@@ -2250,7 +2248,6 @@ typedef struct {
22502248
PyObject_HEAD
22512249
buffered *reader;
22522250
buffered *writer;
2253-
PyObject *dict;
22542251
PyObject *weakreflist;
22552252
} rwpair;
22562253

@@ -2309,10 +2306,9 @@ bufferedrwpair_traverse(PyObject *op, visitproc visit, void *arg)
23092306
{
23102307
rwpair *self = rwpair_CAST(op);
23112308
Py_VISIT(Py_TYPE(self));
2312-
Py_VISIT(self->dict);
23132309
Py_VISIT(self->reader);
23142310
Py_VISIT(self->writer);
2315-
return 0;
2311+
return PyObject_VisitManagedDict(op, visit, arg);
23162312
}
23172313

23182314
static int
@@ -2321,7 +2317,7 @@ bufferedrwpair_clear(PyObject *op)
23212317
rwpair *self = rwpair_CAST(op);
23222318
Py_CLEAR(self->reader);
23232319
Py_CLEAR(self->writer);
2324-
Py_CLEAR(self->dict);
2320+
PyObject_ClearManagedDict(op);
23252321
return 0;
23262322
}
23272323

@@ -2639,7 +2635,6 @@ static PyMemberDef bufferedreader_members[] = {
26392635
{"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY},
26402636
{"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0},
26412637
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY},
2642-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY},
26432638
{NULL}
26442639
};
26452640

@@ -2698,7 +2693,6 @@ static PyMemberDef bufferedwriter_members[] = {
26982693
{"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY},
26992694
{"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0},
27002695
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY},
2701-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY},
27022696
{NULL}
27032697
};
27042698

@@ -2752,7 +2746,6 @@ static PyMethodDef bufferedrwpair_methods[] = {
27522746

27532747
static PyMemberDef bufferedrwpair_members[] = {
27542748
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(rwpair, weakreflist), Py_READONLY},
2755-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(rwpair, dict), Py_READONLY},
27562749
{NULL}
27572750
};
27582751

@@ -2815,7 +2808,6 @@ static PyMemberDef bufferedrandom_members[] = {
28152808
{"raw", _Py_T_OBJECT, offsetof(buffered, raw), Py_READONLY},
28162809
{"_finalizing", Py_T_BOOL, offsetof(buffered, finalizing), 0},
28172810
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(buffered, weakreflist), Py_READONLY},
2818-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(buffered, dict), Py_READONLY},
28192811
{NULL}
28202812
};
28212813

‎Modules/_io/bytesio.c‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ typedef struct {
2121
PyObject *buf;
2222
Py_ssize_t pos;
2323
Py_ssize_t string_size;
24-
PyObject *dict;
2524
PyObject *weakreflist;
2625
Py_ssize_t exports;
2726
#ifdef Py_GIL_DISABLED
@@ -974,11 +973,16 @@ _io_BytesIO___getstate___impl(bytesio *self)
974973

975974
if (initvalue == NULL)
976975
return NULL;
977-
if (self->dict == NULL) {
978-
dict = Py_NewRef(Py_None);
976+
dict = PyObject_GenericGetDict((PyObject *)self, NULL);
977+
if (dict == NULL) {
978+
Py_DECREF(initvalue);
979+
return NULL;
980+
}
981+
if (PyDict_GET_SIZE(dict) == 0) {
982+
Py_SETREF(dict, Py_NewRef(Py_None));
979983
}
980984
else {
981-
dict = PyDict_Copy(self->dict);
985+
Py_SETREF(dict, PyDict_Copy(dict));
982986
if (dict == NULL) {
983987
Py_DECREF(initvalue);
984988
return NULL;
@@ -1061,16 +1065,16 @@ _io_BytesIO___setstate___impl(bytesio *self, PyObject *state)
10611065
Py_TYPE(dict)->tp_name);
10621066
return NULL;
10631067
}
1064-
if (self->dict) {
1065-
/* Alternatively, we could replace the internal dictionary
1066-
completely. However, it seems more practical to just update it. */
1067-
if (PyDict_Update(self->dict, dict) < 0)
1068-
return NULL;
1068+
/* Alternatively, we could replace the internal dictionary
1069+
completely. However, it seems more practical to just update it. */
1070+
PyObject *self_dict = PyObject_GenericGetDict((PyObject *)self, NULL);
1071+
if (self_dict == NULL) {
1072+
return NULL;
10691073
}
1070-
else {
1071-
/* The LOAD_ATTR specializations read the dict slot lock-free
1072-
with an acquire load, so pair it with a release store. */
1073-
FT_ATOMIC_STORE_PTR_RELEASE(self->dict, Py_NewRef(dict));
1074+
int rc = PyDict_Update(self_dict, dict);
1075+
Py_DECREF(self_dict);
1076+
if (rc < 0) {
1077+
return NULL;
10741078
}
10751079
}
10761080

@@ -1089,7 +1093,7 @@ bytesio_dealloc(PyObject *op)
10891093
PyErr_Print();
10901094
}
10911095
Py_CLEAR(self->buf);
1092-
Py_CLEAR(self->dict);
1096+
PyObject_ClearManagedDict(op);
10931097
FT_CLEAR_WEAKREFS(op, self->weakreflist);
10941098
tp->tp_free(self);
10951099
Py_DECREF(tp);
@@ -1186,16 +1190,15 @@ bytesio_traverse(PyObject *op, visitproc visit, void *arg)
11861190
{
11871191
bytesio *self = bytesio_CAST(op);
11881192
Py_VISIT(Py_TYPE(self));
1189-
Py_VISIT(self->dict);
11901193
Py_VISIT(self->buf);
1191-
return 0;
1194+
return PyObject_VisitManagedDict(op, visit, arg);
11921195
}
11931196

11941197
static int
11951198
bytesio_clear(PyObject *op)
11961199
{
11971200
bytesio *self = bytesio_CAST(op);
1198-
Py_CLEAR(self->dict);
1201+
PyObject_ClearManagedDict(op);
11991202
if (FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) == 0) {
12001203
Py_CLEAR(self->buf);
12011204
}
@@ -1240,7 +1243,6 @@ static struct PyMethodDef bytesio_methods[] = {
12401243

12411244
static PyMemberDef bytesio_members[] = {
12421245
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(bytesio, weakreflist), Py_READONLY},
1243-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(bytesio, dict), Py_READONLY},
12441246
{NULL}
12451247
};
12461248

‎Modules/_io/fileio.c‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ typedef struct {
8181
gh-109523). */
8282
struct _Py_stat_struct *stat_atopen;
8383
PyObject *weakreflist;
84-
PyObject *dict;
8584
} fileio;
8685

8786
#define PyFileIO_Check(state, op) (PyObject_TypeCheck((op), state->PyFileIO_Type))
@@ -558,15 +557,13 @@ fileio_traverse(PyObject *op, visitproc visit, void *arg)
558557
{
559558
fileio *self = PyFileIO_CAST(op);
560559
Py_VISIT(Py_TYPE(self));
561-
Py_VISIT(self->dict);
562-
return 0;
560+
return PyObject_VisitManagedDict(op, visit, arg);
563561
}
564562

565563
static int
566564
fileio_clear(PyObject *op)
567565
{
568-
fileio *self = PyFileIO_CAST(op);
569-
Py_CLEAR(self->dict);
566+
PyObject_ClearManagedDict(op);
570567
return 0;
571568
}
572569

@@ -1367,7 +1364,6 @@ static PyGetSetDef fileio_getsetlist[] = {
13671364
static PyMemberDef fileio_members[] = {
13681365
{"_finalizing", Py_T_BOOL, offsetof(fileio, finalizing), 0},
13691366
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(fileio, weakreflist), Py_READONLY},
1370-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(fileio, dict), Py_READONLY},
13711367
{NULL}
13721368
};
13731369

‎Modules/_io/iobase.c‎

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ class _io._RawIOBase "PyObject *" "clinic_state()->PyRawIOBase_Type"
3333
typedef struct {
3434
PyObject_HEAD
3535

36-
PyObject *dict;
3736
PyObject *weakreflist;
3837
} iobase;
3938

@@ -383,15 +382,13 @@ iobase_traverse(PyObject *op, visitproc visit, void *arg)
383382
{
384383
iobase *self = iobase_CAST(op);
385384
Py_VISIT(Py_TYPE(self));
386-
Py_VISIT(self->dict);
387-
return 0;
385+
return PyObject_VisitManagedDict(op, visit, arg);
388386
}
389387

390388
static int
391389
iobase_clear(PyObject *op)
392390
{
393-
iobase *self = iobase_CAST(op);
394-
Py_CLEAR(self->dict);
391+
PyObject_ClearManagedDict(op);
395392
return 0;
396393
}
397394

@@ -400,8 +397,8 @@ iobase_clear(PyObject *op)
400397
static void
401398
iobase_dealloc(PyObject *op)
402399
{
403-
/* NOTE: since IOBaseObject has its own dict, Python-defined attributes
404-
are still available here for close() to use.
400+
/* NOTE: since the managed dict is only cleared below, Python-defined
401+
attributes are still available here for close() to use.
405402
However, if the derived class declares a __slots__, those slots are
406403
already gone.
407404
*/
@@ -417,7 +414,7 @@ iobase_dealloc(PyObject *op)
417414
PyTypeObject *tp = Py_TYPE(self);
418415
_PyObject_GC_UNTRACK(self);
419416
FT_CLEAR_WEAKREFS(op, self->weakreflist);
420-
Py_CLEAR(self->dict);
417+
PyObject_ClearManagedDict(op);
421418
tp->tp_free(self);
422419
Py_DECREF(tp);
423420
}
@@ -909,7 +906,6 @@ static PyGetSetDef iobase_getset[] = {
909906

910907
static struct PyMemberDef iobase_members[] = {
911908
{"__weaklistoffset__", Py_T_PYSSIZET, offsetof(iobase, weakreflist), Py_READONLY},
912-
{"__dictoffset__", Py_T_PYSSIZET, offsetof(iobase, dict), Py_READONLY},
913909
{NULL},
914910
};
915911

@@ -932,7 +928,7 @@ PyType_Spec _Py_iobase_spec = {
932928
.name = "_io._IOBase",
933929
.basicsize = sizeof(iobase),
934930
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
935-
Py_TPFLAGS_IMMUTABLETYPE),
931+
Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_DICT),
936932
.slots = iobase_slots,
937933
};
938934

0 commit comments

Comments
 (0)