When asyncio was first proposed, many people (not so much on python-ideas, where Guido first suggested it, but on external blogs) had the same reaction: Doing the core reactor loop in Python is going to be way too slow. Something based on libev, like gevent, is inherently going to be much faster.

Then, when Python 3.4 came out, people started benchmarking it. For example, this benchmark shows that handling 100000 redis connections, 50 at a time, sending 3-byte messages, takes 3.12 seconds in asyncio vs. 4.55 in gevent.

And yet, I still see people refusing to believe those benchmarks, or their own benchmarks that they run to prove them wrong.

So, why isn't it slow? Isn't Python really slow at looping? Isn't libev a tightly-optimized reactor core?

The problem is that, as is so common, people are focusing on optimizing the wrong part of the code.

Under the covers, gevent and asyncio do similar things:
  • Call a function like epoll or kqueue to wait on a whole mess of sockets and see which ones are ready for input or output.
  • For each of those sockets:
    • Read the data.
    • Look up a coroutine in some kind of map.
    • Resume that coroutine.
    • Execute the actual Python code in that coroutine up to the next yield from/implicit suspend point.
Now, which parts of that would you expect libev to be able to optimize? Iterating over hundreds of sockets may be orders of magnitude slower in Python, but it's such a tiny percentage of the total time spent that it can't possibly matter. Remember, for each socket in that loop, you're going to make an I/O-bound syscall, do a context switch, and then interpret a bunch of Python code. It's going to be one of those things that takes time, and there's no way to optimize any of them by rewriting some completely irrelevant piece of code in (even tightly optimized) C.

From my own unscientific tests, using Fantix's work-in-progress port of gevent to Python 3 vs. asyncio, there's rarely a significant difference in either direction. I more often see a difference between Python 2.7 and 3.4. If I need to deal with lots of Unicode text, 3.4 is much faster; if I don't actually need to deal with it as Unicode but 3.x makes it hard for me to avoid doing so, 2.7 is much faster; if neither of those is relevant, 3.4 is sometimes significantly faster but sometimes not noticeably so.

There are of course sometimes good reasons to use gevent instead of asyncio. If you've got a bunch of threaded code that you want to convert over to using coroutines, for example, gevent makes it trivial. But using gevent it because you're absolutely sure that asyncio must be slow because Python code is slow, that's silly.
0

Add a comment

Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.