Skip to content

Commit a9d7e55

Browse files
authored
bpo-32357: Optimize asyncio.iscoroutine() for non-native coroutines (#4915)
1 parent a7bd64c commit a9d7e55

File tree

4 files changed

+149
-33
lines changed

4 files changed

+149
-33
lines changed

Lib/asyncio/coroutines.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
__all__ = 'coroutine', 'iscoroutinefunction', 'iscoroutine'
22

3+
import collections.abc
34
import functools
45
import inspect
56
import os
67
import sys
78
import traceback
89
import types
910

10-
from collections.abc import Awaitable, Coroutine
11-
1211
from . import base_futures
1312
from . import constants
1413
from . import format_helpers
@@ -162,7 +161,7 @@ def coro(*args, **kw):
162161
except AttributeError:
163162
pass
164163
else:
165-
if isinstance(res, Awaitable):
164+
if isinstance(res, collections.abc.Awaitable):
166165
res = yield from await_meth()
167166
return res
168167

@@ -199,12 +198,24 @@ def iscoroutinefunction(func):
199198
# Prioritize native coroutine check to speed-up
200199
# asyncio.iscoroutine.
201200
_COROUTINE_TYPES = (types.CoroutineType, types.GeneratorType,
202-
Coroutine, CoroWrapper)
201+
collections.abc.Coroutine, CoroWrapper)
202+
_iscoroutine_typecache = set()
203203

204204

205205
def iscoroutine(obj):
206206
"""Return True if obj is a coroutine object."""
207-
return isinstance(obj, _COROUTINE_TYPES)
207+
if type(obj) in _iscoroutine_typecache:
208+
return True
209+
210+
if isinstance(obj, _COROUTINE_TYPES):
211+
# Just in case we don't want to cache more than 100
212+
# positive types. That shouldn't ever happen, unless
213+
# someone stressing the system on purpose.
214+
if len(_iscoroutine_typecache) < 100:
215+
_iscoroutine_typecache.add(type(obj))
216+
return True
217+
else:
218+
return False
208219

209220

210221
def _format_coroutine(coro):

Lib/test/test_asyncio/test_tasks.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ def __call__(self, *args):
6262
pass
6363

6464

65+
class CoroLikeObject:
66+
def send(self, v):
67+
raise StopIteration(42)
68+
69+
def throw(self, *exc):
70+
pass
71+
72+
def close(self):
73+
pass
74+
75+
def __await__(self):
76+
return self
77+
78+
6579
class BaseTaskTests:
6680

6781
Task = None
@@ -2085,6 +2099,12 @@ def test_create_task_with_noncoroutine(self):
20852099
"a coroutine was expected, got 123"):
20862100
self.new_task(self.loop, 123)
20872101

2102+
# test it for the second time to ensure that caching
2103+
# in asyncio.iscoroutine() doesn't break things.
2104+
with self.assertRaisesRegex(TypeError,
2105+
"a coroutine was expected, got 123"):
2106+
self.new_task(self.loop, 123)
2107+
20882108
def test_create_task_with_oldstyle_coroutine(self):
20892109

20902110
@asyncio.coroutine
@@ -2095,6 +2115,12 @@ def coro():
20952115
self.assertIsInstance(task, self.Task)
20962116
self.loop.run_until_complete(task)
20972117

2118+
# test it for the second time to ensure that caching
2119+
# in asyncio.iscoroutine() doesn't break things.
2120+
task = self.new_task(self.loop, coro())
2121+
self.assertIsInstance(task, self.Task)
2122+
self.loop.run_until_complete(task)
2123+
20982124
def test_create_task_with_async_function(self):
20992125

21002126
async def coro():
@@ -2104,6 +2130,23 @@ async def coro():
21042130
self.assertIsInstance(task, self.Task)
21052131
self.loop.run_until_complete(task)
21062132

2133+
# test it for the second time to ensure that caching
2134+
# in asyncio.iscoroutine() doesn't break things.
2135+
task = self.new_task(self.loop, coro())
2136+
self.assertIsInstance(task, self.Task)
2137+
self.loop.run_until_complete(task)
2138+
2139+
def test_create_task_with_asynclike_function(self):
2140+
task = self.new_task(self.loop, CoroLikeObject())
2141+
self.assertIsInstance(task, self.Task)
2142+
self.assertEqual(self.loop.run_until_complete(task), 42)
2143+
2144+
# test it for the second time to ensure that caching
2145+
# in asyncio.iscoroutine() doesn't break things.
2146+
task = self.new_task(self.loop, CoroLikeObject())
2147+
self.assertIsInstance(task, self.Task)
2148+
self.assertEqual(self.loop.run_until_complete(task), 42)
2149+
21072150
def test_bare_create_task(self):
21082151

21092152
async def inner():
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Optimize asyncio.iscoroutine() and loop.create_task() for non-native
2+
coroutines (e.g. async/await compiled with Cython).
3+
4+
'loop.create_task(python_coroutine)' used to be 20% faster than
5+
'loop.create_task(cython_coroutine)'. Now, the latter is as fast.

Modules/_asynciomodule.c

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ static PyObject *current_tasks;
4949
all running event loops. {EventLoop: Task} */
5050
static PyObject *all_tasks;
5151

52+
/* An isinstance type cache for the 'is_coroutine()' function. */
53+
static PyObject *iscoroutine_typecache;
54+
5255

5356
typedef enum {
5457
STATE_PENDING,
@@ -118,6 +121,71 @@ static PyObject* future_new_iter(PyObject *);
118121
static inline int future_call_schedule_callbacks(FutureObj *);
119122

120123

124+
static int
125+
_is_coroutine(PyObject *coro)
126+
{
127+
/* 'coro' is not a native coroutine, call asyncio.iscoroutine()
128+
to check if it's another coroutine flavour.
129+
130+
Do this check after 'future_init()'; in case we need to raise
131+
an error, __del__ needs a properly initialized object.
132+
*/
133+
PyObject *res = PyObject_CallFunctionObjArgs(
134+
asyncio_iscoroutine_func, coro, NULL);
135+
if (res == NULL) {
136+
return -1;
137+
}
138+
139+
int is_res_true = PyObject_IsTrue(res);
140+
Py_DECREF(res);
141+
if (is_res_true <= 0) {
142+
return is_res_true;
143+
}
144+
145+
if (PySet_Size(iscoroutine_typecache) < 100) {
146+
/* Just in case we don't want to cache more than 100
147+
positive types. That shouldn't ever happen, unless
148+
someone stressing the system on purpose.
149+
*/
150+
if (PySet_Add(iscoroutine_typecache, (PyObject*) Py_TYPE(coro))) {
151+
return -1;
152+
}
153+
}
154+
155+
return 1;
156+
}
157+
158+
159+
static inline int
160+
is_coroutine(PyObject *coro)
161+
{
162+
if (PyCoro_CheckExact(coro)) {
163+
return 1;
164+
}
165+
166+
/* Check if `type(coro)` is in the cache.
167+
Caching makes is_coroutine() function almost as fast as
168+
PyCoro_CheckExact() for non-native coroutine-like objects
169+
(like coroutines compiled with Cython).
170+
171+
asyncio.iscoroutine() has its own type caching mechanism.
172+
This cache allows us to avoid the cost of even calling
173+
a pure-Python function in 99.9% cases.
174+
*/
175+
int has_it = PySet_Contains(
176+
iscoroutine_typecache, (PyObject*) Py_TYPE(coro));
177+
if (has_it == 0) {
178+
/* type(coro) is not in iscoroutine_typecache */
179+
return _is_coroutine(coro);
180+
}
181+
182+
/* either an error has occured or
183+
type(coro) is in iscoroutine_typecache
184+
*/
185+
return has_it;
186+
}
187+
188+
121189
static int
122190
get_running_loop(PyObject **loop)
123191
{
@@ -1778,37 +1846,20 @@ static int
17781846
_asyncio_Task___init___impl(TaskObj *self, PyObject *coro, PyObject *loop)
17791847
/*[clinic end generated code: output=9f24774c2287fc2f input=8d132974b049593e]*/
17801848
{
1781-
PyObject *res;
1782-
17831849
if (future_init((FutureObj*)self, loop)) {
17841850
return -1;
17851851
}
17861852

1787-
if (!PyCoro_CheckExact(coro)) {
1788-
/* 'coro' is not a native coroutine, call asyncio.iscoroutine()
1789-
to check if it's another coroutine flavour.
1790-
1791-
Do this check after 'future_init()'; in case we need to raise
1792-
an error, __del__ needs a properly initialized object.
1793-
*/
1794-
res = PyObject_CallFunctionObjArgs(
1795-
asyncio_iscoroutine_func, coro, NULL);
1796-
if (res == NULL) {
1797-
return -1;
1798-
}
1799-
1800-
int tmp = PyObject_Not(res);
1801-
Py_DECREF(res);
1802-
if (tmp < 0) {
1803-
return -1;
1804-
}
1805-
if (tmp) {
1806-
self->task_log_destroy_pending = 0;
1807-
PyErr_Format(PyExc_TypeError,
1808-
"a coroutine was expected, got %R",
1809-
coro, NULL);
1810-
return -1;
1811-
}
1853+
int is_coro = is_coroutine(coro);
1854+
if (is_coro == -1) {
1855+
return -1;
1856+
}
1857+
if (is_coro == 0) {
1858+
self->task_log_destroy_pending = 0;
1859+
PyErr_Format(PyExc_TypeError,
1860+
"a coroutine was expected, got %R",
1861+
coro, NULL);
1862+
return -1;
18121863
}
18131864

18141865
self->task_fut_waiter = NULL;
@@ -3007,8 +3058,9 @@ module_free(void *m)
30073058
Py_CLEAR(asyncio_InvalidStateError);
30083059
Py_CLEAR(asyncio_CancelledError);
30093060

3010-
Py_CLEAR(current_tasks);
30113061
Py_CLEAR(all_tasks);
3062+
Py_CLEAR(current_tasks);
3063+
Py_CLEAR(iscoroutine_typecache);
30123064

30133065
module_free_freelists();
30143066
}
@@ -3028,6 +3080,11 @@ module_init(void)
30283080
goto fail;
30293081
}
30303082

3083+
iscoroutine_typecache = PySet_New(NULL);
3084+
if (iscoroutine_typecache == NULL) {
3085+
goto fail;
3086+
}
3087+
30313088
#define WITH_MOD(NAME) \
30323089
Py_CLEAR(module); \
30333090
module = PyImport_ImportModule(NAME); \

0 commit comments

Comments
 (0)