| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
Add PyDict_FromKeysAndValues() and PyDict_FromItems() functions.
|
Benchmark on dict creation with Unicode strings:
diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c
index c14f925b4e7..9987bfa41ba 100644
--- a/Modules/_testcapimodule.c
+++ b/Modules/_testcapimodule.c
@@ -2595,6 +2595,133 @@ create_managed_weakref_nogc_type(PyObject *self, PyObject *Py_UNUSED(args))
}
+static PyObject *
+bench_dict_new(PyObject *ob, PyObject *args)
+{
+ Py_ssize_t size, loops;
+ if (!PyArg_ParseTuple(args, "nn", &size, &loops)) {
+ return NULL;
+ }
+
+ PyTime_t t1, t2;
+ PyTime_PerfCounterRaw(&t1);
+ for (Py_ssize_t loop=0; loop < loops; loop++) {
+ PyObject *d = PyDict_New();
+ if (d == NULL) {
+ return NULL;
+ }
+
+ for (Py_ssize_t i=0; i < size; i++) {
+ PyObject *key = PyUnicode_FromFormat("%zi", i);
+ assert(key != NULL);
+
+ PyObject *value = PyLong_FromLong(i);
+ assert(value != NULL);
+
+ assert(PyDict_SetItem(d, key, value) == 0);
+ Py_DECREF(key);
+ Py_DECREF(value);
+ }
+
+ assert(PyDict_Size(d) == size);
+ Py_DECREF(d);
+ }
+ PyTime_PerfCounterRaw(&t2);
+
+ return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject *
+bench_dict_fromkeysandvalues(PyObject *ob, PyObject *args)
+{
+ Py_ssize_t size, loops;
+ if (!PyArg_ParseTuple(args, "nn", &size, &loops)) {
+ return NULL;
+ }
+
+ PyTime_t t1, t2;
+ PyTime_PerfCounterRaw(&t1);
+ for (Py_ssize_t loop=0; loop < loops; loop++) {
+ PyObject **keys = (PyObject **)PyMem_Malloc(size * sizeof(PyObject*));
+ if (keys == NULL) {
+ return NULL;
+ }
+ PyObject **values = (PyObject **)PyMem_Malloc(size * sizeof(PyObject*));
+ if (values == NULL) {
+ return NULL;
+ }
+
+ for (Py_ssize_t i=0; i < size; i++) {
+ PyObject *key = PyUnicode_FromFormat("%zi", i);
+ assert(key != NULL);
+
+ PyObject *value = PyLong_FromLong(i);
+ assert(value != NULL);
+
+ keys[i] = key;
+ values[i] = value;
+ }
+
+ PyObject *d = PyDict_FromKeysAndValues(keys, values, size);
+ assert(d != NULL);
+ Py_DECREF(d);
+
+ for (Py_ssize_t i=0; i < size; i++) {
+ Py_DECREF(keys[i]);
+ Py_DECREF(values[i]);
+ }
+ PyMem_Free(keys);
+ PyMem_Free(values);
+ }
+ PyTime_PerfCounterRaw(&t2);
+
+ return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
+static PyObject *
+bench_dict_fromitems(PyObject *ob, PyObject *args)
+{
+ Py_ssize_t size, loops;
+ if (!PyArg_ParseTuple(args, "nn", &size, &loops)) {
+ return NULL;
+ }
+
+ PyTime_t t1, t2;
+ PyTime_PerfCounterRaw(&t1);
+ for (Py_ssize_t loop=0; loop < loops; loop++) {
+ PyObject **items = (PyObject **)PyMem_Malloc(size * 2 * sizeof(PyObject*));
+ if (items == NULL) {
+ return NULL;
+ }
+
+ for (Py_ssize_t i=0; i < size; i++) {
+ PyObject *key = PyUnicode_FromFormat("%zi", i);
+ assert(key != NULL);
+
+ PyObject *value = PyLong_FromLong(i);
+ assert(value != NULL);
+
+ items[i * 2 ] = key;
+ items[i * 2 + 1] = value;
+ }
+
+ PyObject *d = PyDict_FromItems(items, size);
+ assert(d != NULL);
+ Py_DECREF(d);
+
+ for (Py_ssize_t i=0; i < size * 2; i++) {
+ Py_DECREF(items[i]);
+ }
+ PyMem_Free(items);
+ }
+ PyTime_PerfCounterRaw(&t2);
+
+ return PyFloat_FromDouble(PyTime_AsSecondsDouble(t2 - t1));
+}
+
+
static PyMethodDef TestMethods[] = {
{"set_errno", set_errno, METH_VARARGS},
{"test_config", test_config, METH_NOARGS},
@@ -2691,6 +2818,9 @@ static PyMethodDef TestMethods[] = {
{"toggle_reftrace_printer", toggle_reftrace_printer, METH_O},
{"create_managed_weakref_nogc_type",
create_managed_weakref_nogc_type, METH_NOARGS},
+ {"bench_dict_new", bench_dict_new, METH_VARARGS},
+ {"bench_dict_fromkeysandvalues", bench_dict_fromkeysandvalues, METH_VARARGS},
+ {"bench_dict_fromitems", bench_dict_fromitems, METH_VARARGS},
{NULL, NULL} /* sentinel */
};
Script: import pyperf
import _testcapi
runner = pyperf.Runner()
for size in (1, 10, 100, 1_000, 10_000):
runner.bench_time_func(f'dict-{size:,}', _testcapi.bench_dict_new, size)
|
Sorry, something went wrong.
|
@scoder @davidhewitt: Do these 2 APIs fit your needs to create a dictionary? |
Sorry, something went wrong.
In Cython, we'd probably know up-front whether the keys are all str or not, so the type checking loop may be redundant in many cases and a flag option could avoid it. However, given branch prediction, first time memory load overhead, etc., I doubt that the added time is going to be visible for reasonably sized literal dicts in the real world. The main gain is from avoiding the repeated back and forth through the C-API barrier, which is certainly worth it. |
Sorry, something went wrong.
|
Is the array in FromItems an array of alternating keys & values? I think my comment in #139963 (comment) as to why the offsets might be necessary for guaranteeing correct interpretation of the layout of Rust tuples still stands, at least for PyO3 user code if we wanted to avoid rewriting their inputs into a C-style array. For PyO3's internal creation of dictionaries, we should be able to use any of these functions fine 👍 |
Sorry, something went wrong.
Yes: key1, value1, key2, value2, ..., keyN, valueN.
Can't you modify your code to produce a flat items vector as FromItems() expect? Or produce two arrays, keys and values, for FromKeysAndValues()? |
Sorry, something went wrong.
The problem is that the PyObject* PyDict_NewPresized(Py_ssize_t size, int unicode_keys) API didn't convince the C API Working Group. So I propose higher level APIs, PyDict_FromKeysAndValues() and PyDict_FromItems(), which don't expose implementation details such as unicode_keys.
Right. At the end, proposed APIs are 1.13x faster than calling PyDict_New() + PyDict_SetItem(). |
Sorry, something went wrong.
For PyO3 internal code, yes. However we might want to expose this for users of PyO3. While designing that API, I think I've decided that they will need to do some arranging to the objects anyway. So while I still think having the flexibility to have offsets is nice, please don't block this on me. |
Sorry, something went wrong.
|
I'd like to consider:
|
Sorry, something went wrong.
#139963 implements such API: PyObject* PyDict_FromItems(
PyObject *const *keys,
Py_ssize_t keys_offset,
PyObject *const *values,
Py_ssize_t values_offset,
Py_ssize_t length)Such API is harder to use (more error-prone), and requires more checks. I prefer a simpler API for the two most common use cases. |
Sorry, something went wrong.
Aha, like a batch of PyDict_SetItem() calls, interesting. I'm not sure if it's a good idea to have a single function to create a dictionary or update a dictionary. I think that I would prefer separated functions for that. For example, PyDict_Merge() has an override parameter to decide what to do if a key already exists. Currently, there are already PyDict_Update() and PyDict_Merge() which accept a dictionary or a collection. |
Sorry, something went wrong.
On further thought, I disagree here. I'd vote for the generalized function, especially because it allows arrays of larger structures to pick the keys/values from.
Two functions are also harder to use, in a different way :) What do you mean by requiring more checks?
Yes -- but nothing that accepts C arrays. Anyway, it's good to consider this, but I agree -- separate functions creating & updating make sense. |
Sorry, something went wrong.
|
I close this PR in favor of #139963. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Add PyDict_FromKeysAndValues() and PyDict_FromItems() functions.
API:
📚 Documentation preview 📚: https://cpython-previews--141682.org.readthedocs.build/