19 ms·
Overhead of Python asyncio tasks
- Hendrikto 4y ago> Clearly create_task is as close as you get to free in the Python world, and I would need to look elsewhere for optimizations. Turns out Textual spends far more time processing CSS rules than creating tasks (obvious in retrospect). Takeaways: 1. Creating async tasks is cheap. 2. It is important to confirm intuitions, before acting on them.
- l_theanine 4y agoI haven't used asyncio that much, certainly not in any serious sense, but wouldn't ContextVar lookups be a major factor of performance in serious asyncio code? Using tasks for things that aren't io-bound seems likely to give a false sense of performance superiority when doing basically nothing.
- willm 4y agoI’d be surprised if context vars are more expensive than a dict lookup or two. But I haven’t profiled. Could be wrong.
- johndubchak 4y agoAfter running that code on both a Windows SB3 and major souped up Lenovo running Ubuntu...I just feel inadequate.
- johndubchak 4y agoWindows SB3: 100,000 tasks 177,778 tasks per/s 200,000 tasks 150,588 tasks per/s 300,000 tasks 152,381 tasks per/s 400,000 tasks 134,031 tasks per/s 500,000 tasks 160,804 tasks per/s 600,000 tasks 129,293 tasks per/s
- jayde2767 4y agoUbuntu: 100,000 tasks 155,257 tasks per/s 200,000 tasks 138,569 tasks per/s 300,000 tasks 134,779 tasks per/s 400,000 tasks 144,371 tasks per/s 500,000 tasks 135,672 tasks per/s 600,000 tasks 135,299 tasks per/s 700,000 tasks 146,456 tasks per/s 800,000 tasks 139,192 tasks per/s
- SCUSKU 4y agoM2 Macbook Pro 16GB 16-inch 2023 100,000 tasks 184,167 tasks per/s 200,000 tasks 160,964 tasks per/s 300,000 tasks 165,278 tasks per/s 400,000 tasks 149,577 tasks per/s 500,000 tasks 160,593 tasks per/s 600,000 tasks 168,098 tasks per/s 700,000 tasks 161,837 tasks per/s 800,000 tasks 160,364 tasks per/s 900,000 tasks 149,479 tasks per/s 1,000,000 tasks 155,919 tasks per/s
- zomnoys 4y agoAbove anything, this shows the performance gains from 3.10 -> 3.11: >> python3.10 create_task_overhead.py 100,000 tasks 185,694 tasks per/s 200,000 tasks 165,581 tasks per/s 300,000 tasks 170,857 tasks per/s 400,000 tasks 159,081 tasks per/s 500,000 tasks 162,640 tasks per/s 600,000 tasks 158,779 tasks per/s 700,000 tasks 161,779 tasks per/s 800,000 tasks 179,965 tasks per/s 900,000 tasks 160,913 tasks per/s 1,000,000 tasks 162,767 tasks per/s >> python3.11 create_task_overhead.py 100,000 tasks 289,318 tasks per/s 200,000 tasks 265,293 tasks per/s 300,000 tasks 266,011 tasks per/s 400,000 tasks 259,821 tasks per/s 500,000 tasks 251,819 tasks per/s 600,000 tasks 267,441 tasks per/s 700,000 tasks 251,789 tasks per/s 800,000 tasks 254,303 tasks per/s 900,000 tasks 249,894 tasks per/s 1,000,000 tasks 266,581 tasks per/s
- nathanasmith 4y agoPython 3.11 running in a-Shell on an M1 iPad Pro Python 3.11.0 (heads/3.11-dirty:8d3dd5b9647, Dec 7 2022, 08:17:48) [Clang 14.0.0 (clang-1400.0.29.202)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> [~/Documents]$ python test.py 100,000 tasks 127,992 tasks per/s 200,000 tasks 115,960 tasks per/s 300,000 tasks 117,205 tasks per/s 400,000 tasks 113,131 tasks per/s 500,000 tasks 109,609 tasks per/s 600,000 tasks 116,649 tasks per/s 700,000 tasks 110,743 tasks per/s 800,000 tasks 111,361 tasks per/s 900,000 tasks 109,688 tasks per/s 1,000,000 tasks 117,064 tasks per/s
- rjmill 4y agoMake sure you set your cpu frequency governor to "performance". If your Linux machine is working an order of magnitude slower than you'd expect from the hardware, that's the first thing I'd check.
- uniqueuid 4y agoasync tasks are cool, but the usual PSA applies here: Be careful to hold your references, because async tasks without active references will be garbage collected. I've been bitten by that in the past. Long discussion here: https://bugs.python.org/issue21163 https://bugs.python.org/issue21163 Docs: https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task https://docs.python.org/3/library/asyncio-task.html#asyncio.... "Important Save a reference to the result of this function, to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done."
- mrnonchalant 4y agoThanks for this!
- jonathan_s 4y agoIf you can, best is to always spawn them in a task group (either using anyio or Python 3.11's task groups). This prevents tasks from being garbage collected, but also prevents situations where components can create tasks that outlive their own lifetime. Plus, it's a saner approach when dealing with exception handling and cancellation.
- uniqueuid 4y agoPerhaps I just don't get them, but task groups never really made sense to me. The whole beauty of async tasks is that you can spawn, retry, and consume them lazily. When you create a task group, you again end up waiting on a single long-running last task, desperately trying to fix individual failures and retries that hold up the entire group.
- d0mine 4y agoHere's why task groups may be a good idea https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g...
- 4y ago
- tpmx 4y agoMostly a testament to how absurdly fast modern CPUs are despite Python itself being so slow. / Recent Python convert, in spite of the horrible general performance of the official implementation of the language. That sweet, sweet module library. Also, with Docker containers the deployment issues have been solved. It might be slow to execute but it's really efficient to develop with.
- selcuka 4y agoThis has always been the case. Computing power doubles every few years, and it's cheap (probably less than 10% of your total project cost, unless you are doing highly specific stuff). It doesn't make sense to optimise for CPU cycles as the real bottleneck is usually developer efficiency, I/O, and UX.
- morelisp 4y agoInvest in Apple, their phone is going to be a big deal.
- crabbone 4y agoReading this is like reading early Renaissance alchemist arguing about how much mercury they need to combine with how much silver to create gold... This is so far gone I don't even know where to begin... > It may be IO that gives AsyncIO its name, but Textual doesn't do any IO of its own. So why on Earth are you using AsyncIO? You don't need it, if that's true... > Those tasks are used to power message queues How are your message queues not doing I/O? What on Earth are they doing then? Needless to say that the whole benchmark is worthless because it never even initiates anything that would be involved when creating actual asynchronous I/O tasks... ---- I mean, I know, in Pythonland this is just your average Wednesday, but dear lord, if you don't visit that land all that often it shocks you more every time you do.
- vore 4y agoasyncio is really not all about I/O though, despite the name. You can just as easily use it for concurrency by interleaving tasks with each other. You can imagine multiple UI elements that all need to make progress, and using coroutines to have them cooperatively yield to each other and not just have one element block progress everywhere else. It's just using asyncio as a task scheduler, nothing more, nothing less. Maybe when you visit Pythonland you might instead be shocked in a more positive way :-)
- crabbone 4y ago[flagged]
- vore 4y agoI can't help but feel this is a low-effort troll, but I'll bite anyway. > Where does this nonsense come from? Seriously? Who told you that? Where is the I/O in this? async def task1(): while True: print("a") await asyncio.sleep(0) async def task2(): while True: print("b") await asyncio.sleep(0) async def main(): t1 = asyncio.create_task(task1()) t2 = asyncio.create_task(task2()) await asyncio.gather(t1, t2) asyncio.run(main()) Fundamentally it's an event loop that allows you to register interest in I/O events. You can, of course, also just register no interest in any I/O events, where you still get scheduler features like timers and cooperative scheduling. > This is wishful thinking. No. It doesn't work. Will not work, unless Python is purged of like 99% of it's current stuff and replaced with something else entirely. It's ridiculous to even consider this possibility applied to the concrete runtime you'd have to work with. Where is the wishful thinking in the same code example I posted? You are free to control cooperation between your tasks as required. > Come on man... you just put "cooperatively" and "will not block" in the same sentence. Of course it doesn't work like that. Will not work, especially in Python where you have to yield control explicitly. It's all toys. None of it works. So? Just yield control explicitly? Just because the multitasking isn't preemptive doesn't mean you can't cooperatively make progress on multiple tasks. > Well, that's stupid. Why would anyone want to do that? It simply won't work. It works fine? The post is literally talking about how it works? Maybe you don't like the ergonomics but that's not a reason to be so dismissive of it. In any case, just because you don't like it doesn't mean there is zero value in this, or that you can be such a massive piece of shit and respond like this.
- t3pfaff 4y agoWhy would you want a terminal emulator anywhere near python? Using python for lightweight system utility gui apps seems like using a hammer to screw in a nail. Yeah you can do it and modern hardware is fast enough that you probably won't care, but why??
- traverseda 4y agoPython is a really good scripting language? I guess the big alternatives would be C and BASH and I'd pick python for short utilities over both of those any day. How slow do you think python is?
- t3pfaff 4y agoI agree that it is a good scripting language. That's where it excells. A GUI terminal emulator however, is not a script... Python once compiled and using it's underlying c libraries is fast enough by modern standards but it is slow to start and projects using it are prone towards difficult to read/messy code. Obviously the latter is up to the developer(s) and whatever standards they set for themselves but any software project tends towards paths of least resistance inherent in the language and frameworks being used over time as maintainers change and PRs fixes from contributers are merged. This is pedantic of course, but that doesn't change the fact that Python is not the optimal solution for this problem.
- mixmastamyk 4y agoYou seem to be off by an order or three of magnitude about what is "fast enough" for terminal apps, which haven't been a performance bottleneck for decades. 200k ops per second not fast enough for you? How many words per second do you type? So why wouldn't you use it? I've have since the late 90s and even then it was faster than I could keep up with. Unoptimized Java Swing on SGI was the only thing that wasn't, from memory. More recently Windows Terminal had a very bad implementation for a while, but fixed it after their ass was handed to them over it here at HN.
- 4y ago
- schmichael 4y agoWhat a fun experiment. I quick converted it to Go using goroutines and waitgroups for fun: https://gist.github.com/schmichael/1a417808b8e88b684838ae9f4cd5d8be#file-out-txt https://gist.github.com/schmichael/1a417808b8e88b684838ae9f4...
- bob1029 4y agoI am seeing 500~600k tasks/second in .NET6 w/ TPL. Edit: Updating per request below. Tested on a TR2950x. I wonder if NUMA issues in my case or bad python version (3.9.4). Python 100,000 tasks 77,108 tasks per/s 200,000 tasks 69,945 tasks per/s 300,000 tasks 72,453 tasks per/s 400,000 tasks 74,636 tasks per/s 500,000 tasks 66,253 tasks per/s 600,000 tasks 77,576 tasks per/s 700,000 tasks 69,673 tasks per/s 800,000 tasks 68,176 tasks per/s 900,000 tasks 73,846 tasks per/s 1,000,000 tasks 68,013 tasks per/s .NET 6 100000 Tasks 523000 Tasks/s 200000 Tasks 550000 Tasks/s 300000 Tasks 550000 Tasks/s 400000 Tasks 559000 Tasks/s 500000 Tasks 547000 Tasks/s 600000 Tasks 539000 Tasks/s 700000 Tasks 547000 Tasks/s 800000 Tasks 540000 Tasks/s 900000 Tasks 560000 Tasks/s 1000000 Tasks 542000 Tasks/s
- lunfard000 4y agowould you mind sharing the go and python results running on your machine too? It is apples to orange comparation otherwise. EDIT. My results on a 5950x (undervolted) python3.8.exe test.py 100,000 tasks 139,130 tasks per/s 200,000 tasks 121,905 tasks per/s 300,000 tasks 120,000 tasks per/s 400,000 tasks 114,286 tasks per/s 500,000 tasks 119,403 tasks per/s 600,000 tasks 117,073 tasks per/s 700,000 tasks 130,612 tasks per/s 800,000 tasks 122,488 tasks per/s 900,000 tasks 120,000 tasks per/s 1,000,000 tasks 110,155 tasks per/s python3.11.exe .\test.py 100,000 tasks 206,452 tasks per/s 200,000 tasks 185,507 tasks per/s 300,000 tasks 186,408 tasks per/s 400,000 tasks 179,021 tasks per/s 500,000 tasks 167,539 tasks per/s 600,000 tasks 177,778 tasks per/s 700,000 tasks 188,235 tasks per/s 800,000 tasks 180,919 tasks per/s 900,000 tasks 168,421 tasks per/s .\test.exe (go 1.20 compiled) 100000 tasks 2710563.336378 tasks per/s 200000 tasks 3076885.207567 tasks per/s 300000 tasks 3332292.917434 tasks per/s 400000 tasks 3040479.422795 tasks per/s 500000 tasks 2810232.844653 tasks per/s 600000 tasks 3004138.200371 tasks per/s 700000 tasks 2738877.029117 tasks per/s 800000 tasks 2893730.985022 tasks per/s 900000 tasks 3043877.494077 tasks per/s 1000000 tasks 2857992.089078 tasks per/s
- jstx1 4y agoAsync Python is still confusing af - when do I need it, what happens under the hood, does it actually help with performance, sometimes the GIL comes into play and sometimes it doesn't, why do we ever use threads at all if there's a GIL, why is it called asyncio if we can use it for anything. My mind is kind of scattered and people seems to be using a lot of async Python for some reason. Any good resources to clear things up?
- bombolo 4y agoIt's basically a fancy way to do epoll() around file descriptors, but hide the need to keep a main loop and state, and keep it hidden as functions that run in fake concurrency, stopping whenever they block, and being executed again when their file descriptor has activity. It doesn't necessarily improve performance. It's just a much easier way to do non-blocking I/O (note that blocking and threaded I/O is easier to do but much much heavier).
- 323 4y agoIt makes concurrent programming much simpler than using threads. Very few locking and care is needed with asyncio, as opposed to using threads. Race conditions are basically not a thing if you write reasonably idiomatic code. It might be (or not) faster than using threads, but that's not the main benefit in my view - this easiness of use is.
- philote 4y agoIt's great for when you can do concurrent I/O tasks. For example running a web backend, web scraping, or many API calls. FastAPI (and Starlette that it's built off of) is an async web framework and in my experience performs well. Basically, your program will normally halt when doing I/O, and won't proceed until that I/O is done. During that halt, your program is doing nothing (no cpu being used). With asyncio, you can schedule multiple tasks to run, so if one is halted doing I/O, another can run. Edit: And AFAIK, the GIL does not come into play at all with async. Only when multithreading.
- morelisp 4y ago> the GIL does not come into play at all with async. In the sense that the GIL is still held and you can have at most one path of Python code executing at a time regardless of whether you use async or threads, sure. Most blocking I/O was already releasing the GIL so the difference is purely in how you can design your modules; for any reasoning about performance the GIL behaves the same way whether you use asyncio or not.
- TickleSteve 4y agoSo, 250,000 per second on a (roughly 10,000 MIPS I7 core) Each of those task_create calls is roughly 10,000,000,000 / 250,000 = 40,000 instructions. Thats 40'000 instructions of pure overhead as it does not contribute to the task at hand (accidental complexity).
- schmichael 4y agoYour method of estimating instructions includes printing multiple lines which context switch to the kernel to perform IO. I'm not sure how an "instruction count" metric is useful anyway. edit: I'm not actually sure you are counting the context switch, but I still don't think estimating instruction count that way is particularly useful.
- TickleSteve 4y agoIts useful to show how much waste there is in a solution. Having an operation that can be executing 250,000 times per second on a modern processor is extremely slow... not fast.
- 323 4y agoWaste allows scale, that's a general civilizational rule. You wouldn't be able to write your comment if the browser were written in extremely efficient assembly code because it wouldn't exist. NASA and every big organization also has a lot of waste, but only that way you can get to the moon.
- schmichael 4y ago250k/s is roughly the same speed as context switching, so while slow for pure computation, it is a reasonable amount of "waste" for switching between concurrent tasks. If you didn't prevent preemptive context switches during your benchmarking, it's entirely possible the only thing you measured was the context switch time. This is a fun experiment, but to get a rigorous idea of the overhead involved takes more work than what anyone in the post or comments has done.
- mkl95 4y agoPython is my strongest language. If you ask me to write some asynchronous code I will try my best not to write it in Python. Usually it's just not worth it.
- philote 4y agoHave you not used async in Python lately? IMO it's very easy to do.
- Adiqq 4y agoI don't have huge experience with Python, but I used async code with C#/Typescript and lately I had to use some asyncio magic. I found this article: https://blog.dalibo.com/2022/09/12/monitoring-python-subprocesses.html https://blog.dalibo.com/2022/09/12/monitoring-python-subproc... and while async/await syntax is the same, it's not entirely clear for me, why there's some event loop and what exactly happens, when I pass function to asyncio.run(), like here: https://github.com/pallets/click/issues/85#issuecomment-503464628 https://github.com/pallets/click/issues/85#issuecomment-5034... So, you can use it and it's not that hard, but there are some parts that are vague for me, no matter which language implements async support.
- HyperSane 4y agoI find threading to be much easier to use in Python
- chinaman425 4y ago[dead]
- brrrrrm 4y agoJavaScript is 10-37x faster out of the box without any imports async function time_tasks(count=100) { async function nop_task() { return performance.now(); } const start = performance.now() let tasks = Array(count).map(nop_task) await Promise.all(tasks) const elapsed = performance.now() - start return elapsed / 1e3 } for (let count = 100000; count < 1000000 + 1; count += 100000) { const ct = await time_tasks(count) console.log(`${count}: ${1 / (ct / count)} tasks/sec`) } Outputs (Python 3.11, Bun 0.5.1): % bun textual.ts 100000: 3767797.000743159 tasks/sec 200000: 9001406.4697609 tasks/sec 300000: 8281002.001242148 tasks/sec 400000: 10038491.340232708 tasks/sec 500000: 8976653.913474608 tasks/sec 600000: 10437550.828698047 tasks/sec 700000: 9443895.154523576 tasks/sec 800000: 11021991.118011119 tasks/sec 900000: 9790550.215324111 tasks/sec 1000000: 10263937.143648934 tasks/sec % python3 textual.py 100,000 tasks 303,063 tasks per/s 200,000 tasks 270,058 tasks per/s 300,000 tasks 271,621 tasks per/s 400,000 tasks 261,945 tasks per/s 500,000 tasks 251,070 tasks per/s 600,000 tasks 272,520 tasks per/s 700,000 tasks 250,977 tasks per/s 800,000 tasks 253,131 tasks per/s 900,000 tasks 244,696 tasks per/s 1,000,000 tasks 266,061 tasks per/s
- seanw444 4y agoBun* is that much faster. Wonder what Node or Deno perform like.
- emmelaich 4y agoFWIW, Mac Air, dual core i7 1.7GHz $ deno run tasks.js 100000: 2777777.777777778 tasks/sec 200000: 3225806.4516129033 tasks/sec ... 800000: 2395209.580838323 tasks/sec 900000: 1679104.4776119404 tasks/sec 1000000: 1851851.8518518517 tasks/sec
- brrrrrm 4y agoHmm, slower as the number scales up. Wonder why my M1 didn’t do that
- 4y ago
- Kab1r 4y agoI have been looking into the overhead if async on c++ and we found that the cost increases substantially when the function has a return value. It would be interesting to see if this is the case with python 's asyncio.
- masklinn 4y agoSeems pretty decent, with that it’s a real shame Python’s async ended up coroutine-based rather than task-based. Given the langage semantics it ends up being a lot of pain for fairly little gain at the end if the day.
- icedchai 4y agoI preferred gevent (it's been probably 10 years since I've used it.) Yes, you need a ton of monkey patching, etc... but it was less intrusive once you had everything set up. Sprinkling await and async everywhere always struck me as inelegant.
- INTPenis 4y agoWhenever I had to run a lot of Python tasks I preferred Celery over asyncio. I only ever write an asyncio daemon when I want to launch and manage the results of a bunch of Celery tasks. Not speaking from some superior position of research here, I'm just saying what I prefer to use.
- jimmylt 4y agoI've tested all the methods I know to wait for an asynchronous task. See: https://gist.github.com/jimmy-lt/4a3c6ad9cab1545692e5a3fe97145537 https://gist.github.com/jimmy-lt/4a3c6ad9cab1545692e5a3fe971... $ python3.11 Synchronous 100,000 tasks 22,716,947 tasks per/s 200,000 tasks 22,706,630 tasks per/s 300,000 tasks 22,742,779 tasks per/s 400,000 tasks 22,614,202 tasks per/s 500,000 tasks 22,760,379 tasks per/s 600,000 tasks 22,799,818 tasks per/s 700,000 tasks 22,842,971 tasks per/s 800,000 tasks 22,778,395 tasks per/s 900,000 tasks 22,854,241 tasks per/s 1,000,000 tasks 22,470,395 tasks per/s await 100,000 tasks 10,336,986 tasks per/s 200,000 tasks 10,405,286 tasks per/s 300,000 tasks 10,451,505 tasks per/s 400,000 tasks 10,482,455 tasks per/s 500,000 tasks 10,451,287 tasks per/s 600,000 tasks 10,485,478 tasks per/s 700,000 tasks 10,508,302 tasks per/s 800,000 tasks 10,505,167 tasks per/s 900,000 tasks 10,492,568 tasks per/s 1,000,000 tasks 10,457,516 tasks per/s asyncio.create_task() 100,000 tasks 219,858 tasks per/s 200,000 tasks 196,281 tasks per/s 300,000 tasks 201,530 tasks per/s 400,000 tasks 193,674 tasks per/s 500,000 tasks 187,611 tasks per/s 600,000 tasks 201,972 tasks per/s 700,000 tasks 187,505 tasks per/s 800,000 tasks 191,531 tasks per/s 900,000 tasks 198,127 tasks per/s 1,000,000 tasks 173,259 tasks per/s asyncio.gather() 100,000 tasks 291,095 tasks per/s 200,000 tasks 193,324 tasks per/s 300,000 tasks 129,177 tasks per/s 400,000 tasks 107,024 tasks per/s 500,000 tasks 123,023 tasks per/s 600,000 tasks 122,304 tasks per/s 700,000 tasks 121,674 tasks per/s 800,000 tasks 106,530 tasks per/s 900,000 tasks 135,841 tasks per/s 1,000,000 tasks 106,153 tasks per/s asyncio.TaskGroup.create_task() 100,000 tasks 319,629 tasks per/s 200,000 tasks 283,560 tasks per/s 300,000 tasks 204,328 tasks per/s 400,000 tasks 203,584 tasks per/s 500,000 tasks 200,968 tasks per/s 600,000 tasks 214,506 tasks per/s 700,000 tasks 206,512 tasks per/s 800,000 tasks 204,556 tasks per/s 900,000 tasks 210,298 tasks per/s 1,000,000 tasks 202,523 tasks per/s