Bug report
Bug description:
future_init() in Modules/_asynciomodule.c reads the loop's debug flag by calling get_debug() on every Future creation:
res = PyObject_CallMethodNoArgs(fut->fut_loop, &_Py_ID(get_debug));
For the standard loop, BaseEventLoop.get_debug() is just return self._debug, but it pays for a full method call.
My proposal is to read _debug directly, with fallback to the method-call implementation (in case custom event loops dont have)
if (PyObject_GetOptionalAttr(fut->fut_loop, &_Py_ID(_debug), &res) < 0) {
return -1;
}
if (res == NULL) {
res = PyObject_CallMethodNoArgs(fut->fut_loop, &_Py_ID(get_debug));
if (res == NULL) {
return -1;
}
}
I already benchmarked it and got a solid speedup on the future creation, but for e2e it's about 1-2%:
+----------------+---------+-----------------------+
| Benchmark | before | after |
+================+=========+=======================+
| create_future | 166 ns | 133 ns: 1.24x faster |
+----------------+---------+-----------------------+
| create_task | 26.4 us | 25.9 us: 1.02x faster |
+----------------+---------+-----------------------+
| Geometric mean | (ref) | 1.08x faster |
+----------------+---------+-----------------------+
Benchmark:
import asyncio
import pyperf
def bench_create_future(loops):
loop = asyncio.new_event_loop()
try:
cf = loop.create_future
t0 = pyperf.perf_counter()
for _ in range(loops):
fut = cf()
fut.cancel()
return pyperf.perf_counter() - t0
finally:
loop.close()
def bench_create_task(loops):
async def noop():
return None
async def run():
t0 = pyperf.perf_counter()
for _ in range(loops):
t = asyncio.ensure_future(noop())
await t
return pyperf.perf_counter() - t0
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(run())
finally:
loop.close()
def bench_gather(loops):
N = 100
async def child():
return 1
async def one_round():
await asyncio.gather(*[child() for _ in range(N)])
loop = asyncio.new_event_loop()
try:
t0 = pyperf.perf_counter()
for _ in range(loops):
loop.run_until_complete(one_round())
return pyperf.perf_counter() - t0
finally:
loop.close()
if __name__ == "__main__":
runner = pyperf.Runner()
runner.metadata["description"] = "asyncio Future creation hot paths"
runner.bench_time_func("create_future", bench_create_future)
runner.bench_time_func("create_task", bench_create_task)
runner.bench_time_func("gather_100", bench_gather)
About the reading private attributes from the interp side: this pattern is already used across the stdlib including the asyncio itself e.g get_future_loop :
return PyObject_GetAttr(fut, &_Py_ID(_loop));
I already have pr, but want to know: Is this worth it, or is reading the loop's private _debug (bypassing the get_debug() API) undesirable here?
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS
Bug report
Bug description:
future_init()inModules/_asynciomodule.creads the loop's debug flag by callingget_debug()on every Future creation:For the standard loop,
BaseEventLoop.get_debug()is justreturn self._debug, but it pays for a full method call.My proposal is to read
_debugdirectly, with fallback to the method-call implementation (in case custom event loops dont have)I already benchmarked it and got a solid speedup on the future creation, but for e2e it's about 1-2%:
Benchmark:
About the reading private attributes from the interp side: this pattern is already used across the stdlib including the asyncio itself e.g
get_future_loop:I already have pr, but want to know: Is this worth it, or is reading the loop's private _debug (bypassing the get_debug() API) undesirable here?
CPython versions tested on:
CPython main branch
Operating systems tested on:
macOS