|
|
|
|
|
by solid_fuel
752 days ago
|
|
I'm not really sure what this proves, since there aren't really good reasons for spawning 1 million processes that do nothing except sleeping. A more convincing demonstration would be spawning 1 million state machines that each maintain their own state and process messages or otherwise do useful work. But examples of that on the BEAM have been around for years. So, in interest of matching this code I wrote an example of spawning 1_000_000 processes that each wait for 3 seconds and then exit. This is Elixir, but this is trivial to do on the BEAM and could easily be done in Erlang as well: #!/usr/bin/env elixir
[process_count | _] = System.argv()
count = String.to_integer(process_count)
IO.puts "spawning #{count} processes"
1..count
|> Enum.map(fn _c ->
Task.async(fn ->
Process.sleep(3_000)
end)
end)
|> Task.await_many()
The default process limit is 262,000-ish for historical reasons but it is easy to override when running the script: ยป time elixir --erl "+P 1000001" process_demo.exs 1000000
spawning 1000000 processes
________________________________________________________
Executed in 6.85 secs fish external
usr time 11.79 secs 60.00 micros 11.79 secs
sys time 15.81 secs 714.00 micros 15.81 secs
I tried to get dotnet set up on my mac to run the code in your example to provide a timing comparison, but it has been a few years since I wrote C# professionally and I wasn't able to quickly finish the required boilerplate set up to run it.Ultimately, although imo the BEAM performs quite well here, I think these kind of showy-but-simple tests miss the advantages of what OTP provides: unparalleled introspection abilities in production on a running system. Unfortunately, it is more difficult to demonstrate the runtime tools in a small code example. |
|
I have adjusted the example to match yours and be more expensive on .NET - previous one was spawning 1 million tasks waiting for the same asynchronous timer captured by a closure, each with own state machine, but nonetheless as cheap as it gets - spawning an asynchronously yielding C# task still costs 96B[0] even if we count state machine box allocation (closer to 112B in this case iirc).
To match your snippet, this now spawns 1M tasks that wait the respective 1M asynchronous timers, approximately tripling the allocation traffic.
In order to run this, you only need an SDK from https://dot.net/download. You can also get it from homebrew with `brew install dotnet-sdk` but I do not recommend daily driving this type of installation as Homebrew using separate path sometimes conflicts with other tooling and breaks SDK packs discovery of .NET's build system should you install another SDK in a different location.After that, the setup process is just
Note: The use of AOT here is to avoid it spamming files as the default publish mode is "separate file per assembly + host-provided runtime" which is not as nice to use (historical default). Otherwise, the impact on the code execution time is minimal. Keep in mind that upon doing the first AOT compilation, it will have to pull IL AOT compiler from nuget feed.Once done, you can just nuke the `/usr/local/share/dotnet` folder if you don't wish to keep the SDK.
Either way, thank you for putting together your comment - Elixir does seem like a REPL-friendly language[1] in many ways similar to F#. It would be impolite for me to not give it a try as you are willing to do the same for .NET.
[0]: https://devblogs.microsoft.com/dotnet/performance-improvemen...
[1]: there exist dotnet fsi as well as dotnet-script which allow using F# and C# for shell files in a similar way, but I found the startup latency of the latter underwhelming even with the cached compilation it does. It's okay, but not sub-100ms an sub-20ms you get with properly compiled JIT and AOT executables.