Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 28 additions & 23 deletions Include/internal/pycore_dict.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,24 +201,13 @@ struct _dictkeysobject {
/* Number of used entries in dk_entries. */
Py_ssize_t dk_nentries;


/* Actual hash table of dk_size entries. It holds indices in dk_entries,
or DKIX_EMPTY(-1) or DKIX_DUMMY(-2).

Indices must be: 0 <= indice < USABLE_FRACTION(dk_size).

The size in bytes of an indice depends on dk_size:

- 1 byte if dk_size <= 0xff (char*)
- 2 bytes if dk_size <= 0xffff (int16_t*)
- 4 bytes if dk_size <= 0xffffffff (int32_t*)
- 8 bytes otherwise (int64_t*)

Dynamically sized, SIZEOF_VOID_P is minimum. */
char dk_indices[]; /* char is required to avoid strict aliasing. */

/* "PyDictKeyEntry or PyDictUnicodeEntry dk_entries[USABLE_FRACTION(DK_SIZE(dk))];" array follows:
see the DK_ENTRIES() / DK_UNICODE_ENTRIES() functions below */
/* The actual hash table (dk_indices) is stored immediately before this
struct in memory (negative offsets from dk); see _DK_INDICES_BASE().
The entries array is stored here, at the end of the struct. */
union {
PyDictKeyEntry entries[1];
PyDictUnicodeEntry unicode_entries[1];
} dk_entries;
};

/* This must be no more than 250, for the prefix size to fit in one byte. */
Expand Down Expand Up @@ -246,19 +235,35 @@ struct _dictvalues {
#define DK_SIZE(dk) (1<<DK_LOG_SIZE(dk))
#endif

static inline const void* _DK_INDICES_CONST_BASE(const PyDictKeysObject *dk) {
size_t indices_size = (size_t)1 << dk->dk_log2_index_bytes;
return (const char *)dk - indices_size;
}

static inline void* _DK_INDICES_BASE(PyDictKeysObject *dk) {
return (void *)_DK_INDICES_CONST_BASE(dk);
}

static inline const void* _DK_ALLOC_CONST_BASE(const PyDictKeysObject *dk) {
return _DK_INDICES_CONST_BASE(dk);
}

static inline void* _DK_ALLOC_BASE(PyDictKeysObject *dk) {
return (void *)_DK_ALLOC_CONST_BASE(dk);
}

static inline void* _DK_ENTRIES(PyDictKeysObject *dk) {
int8_t *indices = (int8_t*)(dk->dk_indices);
size_t index = (size_t)1 << dk->dk_log2_index_bytes;
return (&indices[index]);
return (void *)(&dk->dk_entries);
}

static inline PyDictKeyEntry* DK_ENTRIES(PyDictKeysObject *dk) {
assert(dk->dk_kind == DICT_KEYS_GENERAL);
return (PyDictKeyEntry*)_DK_ENTRIES(dk);
return dk->dk_entries.entries;
}

static inline PyDictUnicodeEntry* DK_UNICODE_ENTRIES(PyDictKeysObject *dk) {
assert(dk->dk_kind != DICT_KEYS_GENERAL);
return (PyDictUnicodeEntry*)_DK_ENTRIES(dk);
return dk->dk_entries.unicode_entries;
}

#define DK_IS_UNICODE(dk) ((dk)->dk_kind != DICT_KEYS_GENERAL)
Expand Down
15 changes: 15 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,21 @@ def __hash__(self):
self.assertEqual(dict_getitem_knownhash(d, k1, hash(k1)), 1)
self.assertRaises(Exc, dict_getitem_knownhash, d, k2, hash(k2))

@support.cpython_only
def test_indices_layout(self):
_testinternalcapi = import_helper.import_module('_testinternalcapi')
check_layout = _testinternalcapi.dict_check_indices_layout

dicts = [
{},
{i: i for i in range(10)},
{i: i for i in range(200)},
{i: i for i in range(2000)},
{i: i for i in range(70000)},
]
for d in dicts:
with self.subTest(size=len(d)):
self.assertTrue(check_layout(d))

from test import mapping_tests

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Restructure ``PyDictKeysObject`` memory layout so the indices array is stored
before the object header, and update dict index access accordingly. In local
dict-operation microbenchmarks this was about 1.4% faster overall, with most
operations improving by roughly 1-2% (:gh:`142889`).
50 changes: 50 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1905,6 +1905,55 @@ dict_getitem_knownhash(PyObject *self, PyObject *args)
return Py_XNewRef(result);
}

static size_t
dict_index_bytes_for_keys(const PyDictKeysObject *keys)
{
int index_shift = keys->dk_log2_index_bytes - DK_LOG_SIZE(keys);
if (index_shift == 0) {
return 1;
}
if (index_shift == 1) {
return 2;
}
if (index_shift == 3) {
#if SIZEOF_VOID_P > 4
return 8;
#endif
/* Py_EMPTY_KEYS uses dk_log2_index_bytes=3 even on 32-bit builds. */
return 4;
}
assert(index_shift == 2);
return 4;
}

static PyObject*
dict_check_indices_layout(PyObject *self, PyObject *arg)
{
if (!PyAnyDict_Check(arg)) {
PyErr_SetString(PyExc_TypeError, "expected a dict");
return NULL;
}

PyDictObject *mp = (PyDictObject *)arg;
PyDictKeysObject *keys = mp->ma_keys;

size_t indices_size = (size_t)1 << keys->dk_log2_index_bytes;
const char *base = (const char *)_DK_ALLOC_CONST_BASE(keys);
char *header = (char *)keys;
char *entries = (char *)_DK_ENTRIES(keys);

bool ok = true;
ok &= (header == base + indices_size);
ok &= (entries == header + offsetof(PyDictKeysObject, dk_entries));

size_t index_bytes = dict_index_bytes_for_keys(keys);
const char *idx_base = (const char *)_DK_INDICES_CONST_BASE(keys);
/* Index 0 is stored immediately before the header. */
char *idx0 = (char *)keys - (ptrdiff_t)index_bytes;
ok &= (idx0 == idx_base + indices_size - (ptrdiff_t)index_bytes);

return PyBool_FromLong(ok);
}

static int
_init_interp_config_from_object(PyInterpreterConfig *config, PyObject *obj)
Expand Down Expand Up @@ -2904,6 +2953,7 @@ static PyMethodDef module_functions[] = {
{"get_object_dict_values", get_object_dict_values, METH_O},
{"hamt", new_hamt, METH_NOARGS},
{"dict_getitem_knownhash", dict_getitem_knownhash, METH_VARARGS},
{"dict_check_indices_layout", dict_check_indices_layout, METH_O},
{"create_interpreter", _PyCFunction_CAST(create_interpreter),
METH_VARARGS | METH_KEYWORDS},
{"destroy_interpreter", _PyCFunction_CAST(destroy_interpreter),
Expand Down
Loading
Loading