Repository: python/cpython
Pull request: gh-103793: Defer formatting task name
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.

Fixes gh-103793

## performance analysis

in a full run of pyperformance, this is 1.00x faster overall, and up to 5% faster on async benchmarks.

full report in [this gist](https://gist.github.com/itamaro/3209189006b5a48bc9dc1207c6046b66).

Problem statement:
Defer string formatting in asyncio Task creation
# Feature or enhancement

When an asyncio Task is created without passing in a `name` (a common case), the init method uses a global counter to generate a task name of the form `"Task-<counter>"`.

It is very common in applications that the task name is never read or used, and string formatting has non-negligible runtime cost, making task creation slower. It would be beneficial to defer the string formatting operation and avoid incurring that overhead during task creation.

This can be done by storing the counter in the task struct, and using it to lazily populate the name in the `get_name` method.

<!-- gh-linked-prs -->
### Linked PRs
* gh-103767
<!-- /gh-linked-prs -->

Hints from issue discussion:

Commit message: remove redundant return

Patch to review:
diff --git a/Doc/whatsnew/3.12.rst b/Doc/whatsnew/3.12.rst
index 373e31b37cd9dc..9208fae3343c54 100644
--- a/Doc/whatsnew/3.12.rst
+++ b/Doc/whatsnew/3.12.rst
@@ -553,6 +553,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
 ========================
diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index c90d32c97add78..6cd55bf6ae82df 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -104,7 +104,8 @@ def __init__(self, coro, *, loop=None, name=None, context=None):
             raise TypeError(f"a coroutine was expected, got {coro!r}")
 
         if name is None:
-            self._name = f'Task-{_task_name_counter()}'
+            # optimization: defer task name formatting to first get_name
+            self._name = None
         else:
             self._name = str(name)
 
@@ -143,6 +144,8 @@ def get_context(self):
         return self._context
 
     def get_name(self):
+        if self._name is None:
+            self._name = f'Task-{_task_name_counter()}'
         return self._name
 
     def set_name(self, value):
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
index 00000000000000..c48348798e7142
--- /dev/null
+++ b/Misc/NEWS.d/next/Core and Builtins/2023-04-24-14-38-16.gh-issue-103793.kqoH6Q.rst	
@@ -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.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index 2476dca6f58ebf..058f47920ada2c 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -2069,8 +2069,9 @@ _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
+        // set task_name to None to indicate deferred formatting
+        Py_INCREF(name);
     } else if (!PyUnicode_CheckExact(name)) {
         name = PyObject_Str(name);
     } else {
@@ -2449,6 +2450,12 @@ _asyncio_Task_get_name_impl(TaskObj *self)
 /*[clinic end generated code: output=0ecf1570c3b37a8f input=a4a6595d12f4f0f8]*/
 {
     if (self->task_name) {
+        if (Py_IsNone(self->task_name)) {
+            asyncio_state *state = get_asyncio_state_by_def((PyObject *)self);
+            PyObject *name = PyUnicode_FromFormat(
+                "Task-%" PRIu64, ++state->task_name_counter);
+            Py_XSETREF(self->task_name, name);
+        }
         return Py_NewRef(self->task_name);
     }
 


Code review comments:
diff_hunk: @@ -2449,6 +2450,12 @@ _asyncio_Task_get_name_impl(TaskObj *self)
 /*[clinic end generated code: output=0ecf1570c3b37a8f input=a4a6595d12f4f0f8]*/
 {
     if (self->task_name) {
+        if (Py_IsNone(self->task_name)) {
+            asyncio_state *state = get_asyncio_state_by_def((PyObject *)self);
+            PyObject *name = PyUnicode_FromFormat(
+                "Task-%" PRIu64, ++state->task_name_counter);
path: Modules/_asynciomodule.c
text: @user1:
`PyUnicode_FromFormat` returns `NULL` upon error; pass the error up.

diff_hunk: @@ -104,7 +104,8 @@ def __init__(self, coro, *, loop=None, name=None, context=None):
             raise TypeError(f"a coroutine was expected, got {coro!r}")
 
         if name is None:
-            self._name = f'Task-{_task_name_counter()}'
+            # optimization: defer task name formatting to first get_name
+            self._name = None
path: Lib/asyncio/tasks.py
text: @user1:
Why do we need to optimize the .py version (which is normally not run since there is a C accelerator version)?

@author:
we definitely don't need to optimize the python version. I assumed we wanted to maintain parity between the versions, but maybe it doesn't matter to this extent (or my assumption was wrong and parity is not a goal).

I will revert the changes to the python version.