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))

Reply via email to