2015-02-11 15:41 GMT+01:00 Andrew Svetlov <[email protected]>:
> Victor, you benchmark example doesn't use *yield from* statement.
> As Guido measured two years ago the software stack (built on top of
> *yield* statements) is 20x times slower than *yield from* for
> recursive level of 5 or something like that.
The original benchmark used "yield from range(0)". It doesn't look a
fair comparison with "def foo(): return 5". Using asyncio doesn't mean
that all functions must use yield from. You can just write:
@coroutine
def func():
return 5
Original benchmark:
http://paste.openstack.org/show/168385/
=> bench_call.py attached to thils email
I tried to write a more fair benchmark using "if 0: yield":
https://bitbucket.org/haypo/asyncio_staging/src/2f89fbdc7c12bd2541071648018ab9d484d79703/bench_generator.py
=> bench_generator.py also attached to this email
IMO comparing yield to yield-from is completly different than
comparing a function call to consuming a generator (a coroutine).
What do you think?
By the way, Trollius has currently the performance issue of recursive
coroutines. It doesn't use yield-from and it currently creates a new
task for each sub-coroutine. I read somewhere that Tornado solved this
issue by following coroutines instead of treating them independently.
We can probably implement the same optimization strategy in trollius.
Victor
def return_with_normal():
"""this illustrates a function calling upon another, and returning
a value."""
def foo():
return 5
def bar():
f1 = foo()
return f1
return bar
def return_with_generator():
"""this illustrates a function calling upon another, and returning
a value, where the first function must contain a single "yield from".
A decorator illustrating the minimal amount of code in order to
retrieve this value is provided; this decorator is vastly simpler
than the asyncio decorator.
"""
def decorate_to_return(fn):
def decorate():
it = fn()
try:
x = next(it)
except StopIteration as y:
return y.args[0]
return decorate
@decorate_to_return
def foo():
yield from range(0)
return 5
def bar():
f1 = foo()
return f1
return bar
return_with_normal = return_with_normal()
return_with_generator = return_with_generator()
import timeit
print(timeit.timeit(
"return_with_generator()",
"from __main__ import return_with_generator", number=10000000))
print(timeit.timeit(
"return_with_normal()",
"from __main__ import return_with_normal", number=10000000))
"""
Results:
yield from: 12.52761328802444
normal: 2.110536064952612
e.g. the basic approach of asyncio adds 1000% overhead to a simple function
with a single return value.
"""#!/usr/bin/env python3
"""
Microbenchmark comparing performances of calling a function and consuming
a function.
Basically, the microbenchmark measures the time to raise an exception
(StopIteration) and then to catch it.
Output on Fedora 21/i7-2600 with Python 3.4.1:
Call a function: 228 ns
Consume a generator: 951 ns
Generator is 4.2x slower
So raise+catch takes 723 nanosecods.
"""
import time
def func(value):
return value
def call_func():
return func(5)
def coroutine(value):
# Deadcode to declare coroutine() as a generator, not as a function
if 0:
yield
return value
def consume_coroutine():
coro = coroutine(5)
try:
next(coro)
except StopIteration as exc:
return exc.value
else:
raise Exception("StopIteration not raised?")
def bench(func):
best = None
loops = 10 ** 5
for run in range(20):
t1 = time.perf_counter()
for loop in range(loops):
func()
t2 = time.perf_counter()
dt = (t2 - t1) / loops
if best is not None:
best = min(dt, best)
else:
best = dt
return best
dt_func = bench(call_func)
dt_coro = bench(consume_coroutine)
print("Call a function: %.0f ns" % (dt_func * 1e9))
print("Consume a generator: %.0f ns" % (dt_coro * 1e9))
print('Generator is %.1fx slower' % (dt_coro / dt_func))