]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-103793: Defer formatting task name (#103767)
authorItamar Ostricher <itamarost@gmail.com>
Sat, 29 Apr 2023 15:20:09 +0000 (08:20 -0700)
committerGitHub <noreply@github.com>
Sat, 29 Apr 2023 15:20:09 +0000 (08:20 -0700)
The default task name is "Task-<counter>" (if no name is passed in during Task creation).
This is initialized in `Task.__init__` (C impl) using string formatting, which can be quite slow.
Actually using the task name in real world code is not very common, so this is wasted init.

Let's defer this string formatting to the first time the name is read (in `get_name` impl),
so we don't need to pay the string formatting cost if the task name is never read.

We don't change the order in which tasks are assigned numbers (if they are) --
the number is set on task creation, as a PyLong instead of a formatted string.

Co-authored-by: Ɓukasz Langa <lukasz@langa.pl>
Doc/whatsnew/3.12.rst
Lib/test/test_asyncio/test_tasks.py
Misc/NEWS.d/next/Core and Builtins/2023-04-24-14-38-16.gh-issue-103793.kqoH6Q.rst [new file with mode: 0644]
Modules/_asynciomodule.c

index 908cf3bb3656919b177b5283a89bb8f1a6cbc853..f4ee30b0d4d9eb83c0c472831bde5c38d3d859d1 100644 (file)
@@ -610,6 +610,9 @@ Optimizations
   replacement strings containing group references by 2--3 times.
   (Contributed by Serhiy Storchaka in :gh:`91524`.)
 
+* Speed up :class:`asyncio.Task` creation by deferring expensive string formatting.
+  (Contributed by Itamar O in :gh:`103793`.)
+
 
 CPython bytecode changes
 ========================
index 31622c91470bcbdefb3b9878aefbba842361d861..6e8a51ce2555d55f4cf76161ddb58a24c8362be9 100644 (file)
@@ -399,6 +399,18 @@ class BaseTaskTests:
         self.loop.run_until_complete(t1)
         self.loop.run_until_complete(t2)
 
+    def test_task_set_name_pylong(self):
+        # test that setting the task name to a PyLong explicitly doesn't
+        # incorrectly trigger the deferred name formatting logic
+        async def notmuch():
+            return 123
+
+        t = self.new_task(self.loop, notmuch(), name=987654321)
+        self.assertEqual(t.get_name(), '987654321')
+        t.set_name(123456789)
+        self.assertEqual(t.get_name(), '123456789')
+        self.loop.run_until_complete(t)
+
     def test_task_repr_name_not_str(self):
         async def notmuch():
             return 123
diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-04-24-14-38-16.gh-issue-103793.kqoH6Q.rst b/Misc/NEWS.d/next/Core and Builtins/2023-04-24-14-38-16.gh-issue-103793.kqoH6Q.rst
new file mode 100644 (file)
index 0000000..c483487
--- /dev/null
@@ -0,0 +1,3 @@
+Optimized asyncio Task creation by deferring expensive string formatting
+(task name generation) from Task creation to the first time ``get_name`` is
+called. This makes asyncio benchmarks up to 5% faster.
index 2476dca6f58ebf1e76f33ed14a43552e9c2318af..82dbc087322aa93179ac733884d58f167b47914d 100644 (file)
@@ -2069,8 +2069,10 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
     Py_XSETREF(self->task_coro, coro);
 
     if (name == Py_None) {
-        name = PyUnicode_FromFormat("Task-%" PRIu64,
-                                    ++state->task_name_counter);
+        // optimization: defer task name formatting
+        // store the task counter as PyLong in the name
+        // for deferred formatting in get_name
+        name = PyLong_FromUnsignedLongLong(++state->task_name_counter);
     } else if (!PyUnicode_CheckExact(name)) {
         name = PyObject_Str(name);
     } else {
@@ -2449,6 +2451,13 @@ _asyncio_Task_get_name_impl(TaskObj *self)
 /*[clinic end generated code: output=0ecf1570c3b37a8f input=a4a6595d12f4f0f8]*/
 {
     if (self->task_name) {
+        if (PyLong_CheckExact(self->task_name)) {
+            PyObject *name = PyUnicode_FromFormat("Task-%S", self->task_name);
+            if (name == NULL) {
+                return NULL;
+            }
+            Py_SETREF(self->task_name, name);
+        }
         return Py_NewRef(self->task_name);
     }