Skip to content

Commit

Permalink
gh-103793: Defer formatting task name (#103767)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
itamaro and ambv authored Apr 29, 2023
1 parent fbf3596 commit 85c7bf5
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 2 deletions.
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.12.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
========================
Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_asyncio/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,18 @@ async def notmuch():
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 11 additions & 2 deletions Modules/_asynciomodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}

Expand Down

0 comments on commit 85c7bf5

Please sign in to comment.