5 ms·
This depends a lot on how you define "concurrent tasks", but the article provides a definition: Let's launch N concurrent tasks, where each task waits for 10 s
by cperciva 2y ago
This depends a lot on how you define "concurrent tasks", but the article provides a definition:
Let's launch N concurrent tasks, where each task waits for 10 seconds and then the program exists after all tasks finish. The number of tasks is controlled by the command line argument.
Leaving aside semantics like "since the tasks aren't specified as doing anything with side effects, the compiler can remove them as dead code", all you really need here is a timer and a continuation for each "task" -- i.e 24 bytes on most platforms. Allowing for allocation overhead and a data structure to manage all the timers efficiently, you might use as much as double that; with some tricks (e.g. function pointer compression) you could get it down to half that.
Eyeballing the graph, it looks like the winner is around 200MB for 1M concurrent tasks, so about 4x worse than a reasonably efficient but not heavily optimized implementation would be.
I have no idea what Go is doing to get 2500 bytes per task.
- liveoneggs 2y agohttps://tpaschalis.me/goroutines-size/ https://tpaschalis.me/goroutines-size/ https://github.com/golang/go/blob/master/src/runtime/stack.go#L75 https://github.com/golang/go/blob/master/src/runtime/stack.g...
- masklinn 2y ago> I have no idea what Go is doing to get 2500 bytes per task. TFA creates a goroutine (green thread) for each task (using a waitgroup to synchronise them). IIRC goroutines default to 2k stacks, so that’s about right. One could argue it’s not fair and it should be timers which would be much lighter. There’s no “efficient wait” for them but that’s essentially the same as the appendix rust program.
- jakewins 2y agoFair or not, it’s a strange way to count - Go isn’t using that RAM. It’s preallocating it because any real world program will.
- winternewt 2y agoPlease elaborate. If each stack is 2KB then surely all of that virtual memory is committed to physical RAM, and hence is using actual memory?
- scrapheap 2y agoYes and no. If that memory isn't being used and other things need the memory then the OS will very quickly dump it into swap, and as it's never being touched the OS will never need to bring it back in to physical memory. So while it's allocated it doesn't tie up the physical RAM.
- masklinn 2y agoGo is absolutely using that ram, it’s not available to other services on the system.
- jakewins 2y agoYou know what I mean. If this was a real world program where those million tasks actually performed work, then this stack space is available for the application to do that work. It’s not memory that’s consumed by the runtime, it’s memory the runtime expects the program to use - it’s just that this program does no useful work.
- aeturnum 2y agoI am not u/masklinn - but I don't know what you mean. Doesn't the runtime consume memory by setting it aside for future use? Like what else does "using" ram mean other than claiming it for a time?
- hatefulmoron 2y agoI think he means that if the Go code had done something more useful, it would use about the same amount of memory. Compare that to another implementation, which might allocate nearly no memory when the tasks don't do anything significant but would quickly catch up to Go if they did.
- cperciva 2y agoAha, 2k stacks. I figured that stacks would be page size (or more) so 2500 seemed both too small for the thread to have a stack and too large for it to not have a stack. 2k stacks are an interesting design choice though... presumably they're packed, in which case stack overflow is a serious concern. Most threading systems will do something like allocating a single page for the stack but reserving 31 guard pages in case it needs to grow.
- YZF 2y agoA goroutine stack can grow. (EDIT: With stack copying AFAICT... so no virtual pages reserved for a stack to grow... probably some reason for this design?)
- masklinn 2y agoGoroutines being go structures, the runtime can cooperate with itself so it doesn't need to do any sort of probing: function prologues can check if there's enough stack space for its frame, and grow the stack if not. In reality it does use a guard area (technically I think it's more of a redzone? It doesn't cause access errors and functions with known small static frames can use it without checking).
- uluyol 2y agoGo stacks are dynamically copied and resized. Stack overflow is not a concern.
- cperciva 2y agoOh yuck. Invalidating all the pointers to the stack? That's got to be expensive. I guess if you're already doing garbage collection moving the stack doesn't make things all that much worse though... still, yuck.
- masklinn 2y agoYeah it’s the drawback, originally it used segmented stacks but that has its own issues. And it’s probably not the worst issue because deep stacks and stack pointers will mostly be relevant for long running routines which will stabilise their stack use after a while (even if some are likely subject to threshold effects if they’re at the edge, I would not be surprised if some codebases ballasted stacks ahead of time). Also because stack pointers will get promoted to the heap if they escape so the number of stack pointers is not unlimited, and the pointer has to live downwards on the stack.