Skip to content

Commit 85c7bf5

Browse files
itamaroambv
andauthored
gh-103793: Defer formatting task name (#103767)
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>
1 parent fbf3596 commit 85c7bf5

File tree

4 files changed

+29
-2
lines changed

4 files changed

+29
-2
lines changed

Doc/whatsnew/3.12.rst

+3
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,9 @@ Optimizations
610610
replacement strings containing group references by 2--3 times.
611611
(Contributed by Serhiy Storchaka in :gh:`91524`.)
612612

613+
* Speed up :class:`asyncio.Task` creation by deferring expensive string formatting.
614+
(Contributed by Itamar O in :gh:`103793`.)
615+
613616

614617
CPython bytecode changes
615618
========================

Lib/test/test_asyncio/test_tasks.py

+12
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,18 @@ async def notmuch():
399399
self.loop.run_until_complete(t1)
400400
self.loop.run_until_complete(t2)
401401

402+
def test_task_set_name_pylong(self):
403+
# test that setting the task name to a PyLong explicitly doesn't
404+
# incorrectly trigger the deferred name formatting logic
405+
async def notmuch():
406+
return 123
407+
408+
t = self.new_task(self.loop, notmuch(), name=987654321)
409+
self.assertEqual(t.get_name(), '987654321')
410+
t.set_name(123456789)
411+
self.assertEqual(t.get_name(), '123456789')
412+
self.loop.run_until_complete(t)
413+
402414
def test_task_repr_name_not_str(self):
403415
async def notmuch():
404416
return 123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Optimized asyncio Task creation by deferring expensive string formatting
2+
(task name generation) from Task creation to the first time ``get_name`` is
3+
called. This makes asyncio benchmarks up to 5% faster.

Modules/_asynciomodule.c

+11-2
Original file line numberDiff line numberDiff line change
@@ -2069,8 +2069,10 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop,
20692069
Py_XSETREF(self->task_coro, coro);
20702070

20712071
if (name == Py_None) {
2072-
name = PyUnicode_FromFormat("Task-%" PRIu64,
2073-
++state->task_name_counter);
2072+
// optimization: defer task name formatting
2073+
// store the task counter as PyLong in the name
2074+
// for deferred formatting in get_name
2075+
name = PyLong_FromUnsignedLongLong(++state->task_name_counter);
20742076
} else if (!PyUnicode_CheckExact(name)) {
20752077
name = PyObject_Str(name);
20762078
} else {
@@ -2449,6 +2451,13 @@ _asyncio_Task_get_name_impl(TaskObj *self)
24492451
/*[clinic end generated code: output=0ecf1570c3b37a8f input=a4a6595d12f4f0f8]*/
24502452
{
24512453
if (self->task_name) {
2454+
if (PyLong_CheckExact(self->task_name)) {
2455+
PyObject *name = PyUnicode_FromFormat("Task-%S", self->task_name);
2456+
if (name == NULL) {
2457+
return NULL;
2458+
}
2459+
Py_SETREF(self->task_name, name);
2460+
}
24522461
return Py_NewRef(self->task_name);
24532462
}
24542463

0 commit comments

Comments
 (0)