Nice article!
People asked a few times how well suited it is in this space in the recent fireship video that went viral
It would be interesting to compile it into a video like "Nim v2 preview in 100 secs" :P
If your domain is not “hard” real-time but “soft” real-time on a conventional OS, you can “pin” a thread to particular core via system.pinToCpu. This can mitigate the jitter conventional operating systems can introduce.
pinToCPU is strongly discouraged on ARM due to the Big.Little architecture (one powerful cores and multiple less powerful core and OS migration) and similarly, it doesn't work well on latest Intel CPUs, 12th (Alder Lake) and 13th (Raptor lake) generation due to varying performance level.
Now the CPU itself has a "Thread director" and collaborate with the OS so pinToCPU can likely be deprecated in Nim v2.
What's the proper way to write in Nim:
for each core in computerPhysCores:
spawn(myApp, core)
C++ Exceptions https://grenouillebouillie.wordpress.com/2022/05/09/the-hidden-cost-of-exception-handling
A very good article but it makes the usual mistake: If you compare to -fnoexceptions you basically end up comparing "correct" to "not correct" code. And behold "not correct" code can be faster! Oh the miracle! A better comparison though is to compare it to code that uses Option[T] or Either[T, E] quite liberally where error handling is required.
you basically end up comparing "correct" to "not correct" code.
not quite: there are two points worth keeping in mind with exceptions: they incur a significant cost no matter if you use them or not - ie the mere possibility of there being an exception raised leads to significant overhead and missed optimization opportunities (because code must be generated defensively) - this becomes evident when you start adorning your code with throw() in C++ or indeed disable them completely - in nim, raises: [] is not enough since we have to deal with defects too that unfortunately use the same exception mechanism.
The second point would be that exceptions will always be at least as slow or slower due to the dynamic typing overhead (vs non-dynamically-typed solutions, based on the assumption that exceptions indeed are heap-allocated and dynamically typed like in Nim). If you ignore all the syntactic differences and write perfect exception-safe code vs perfect return-based code, the latter will be the same or faster.
Finally, empirically and practically, exception use rarely leads to "correct" code since people forget to actually handle them - thus comparisons are often skewed in the favor of exception-based code which has less error handling overall (and still ends up tends to end up slower due to the defensive overhead the compiler has to output).
If you ignore all the syntactic differences and write perfect exception-safe code vs perfect return-based code, the latter will be the same or faster.
Not necessarily:
"In summary, the exceptions approach was 7% smaller and 4% faster as a geomean across our key benchmarks, thanks to a few things:
No calling convention impact. No peanut butter associated with wrapping return values and caller branching. All throwing functions were known in the type system, enabling more flexible code motion. All throwing functions were known in the type system, giving us novel EH optimizations, like turning try/finally blocks into straightline code when the try could not throw. There were other aspects of exceptions that helped with performance. I already mentioned that we didn’t grovel the callstack gathering up metadata as most exceptions systems do. We left diagnostics to our diagnostics subsystem. Another common pattern that helped, however, was to cache exceptions as frozen objects, so that each throw didn’t require an allocation: ..."
From the well known http://joeduffyblog.com/2016/02/07/the-error-model/
How well this translates over to C++'s exception handling implementation is of course not clear but it's simply not as simple as "once correctly written the code with Option/Either performs better". It does not. There are information theoretical reasons for this too: The Option/Either model obfuscates what the hot and what the cold paths look like.
Not necessarily:
Indeed - he talks about typed exceptions, ie where static type analysis remains applicable - this is not the case for Nim and thus we have to pay the price. We can claw some of it back, but in many places we have to make conservative guesses which affect the happy path - on the unhappy path we also have to make a dynamic allocation with subsequent dynamic type discovery - this all adds up.
Yes, returning Result everywhere has an associated cost too - goto-exceptions are nice in that way in that the bool is "mostly" optimizable, but is Result if you exploit knowledge about it (ie make the same assumption about hot/cold as goto-exceptions make) - implementation-wise they're very similar except for the dynamic typing aspect - a Result is after all a bool + a value, but its type is statically known so the compiler / optimizer has more information to work with - with that simple point in hand, it's easy to understand (information-theoretically, if you will) that it will be the same or better than goto-exceptions in all cases, for perfectly generated code.
he talks about typed exceptions, ie where static type analysis remains applicable
In the quoted paragraph he talks about how table based exceptions win performance-wise. Now your argument is that this is only true because Midori has precise exception typings but it's not clear that this information is required and used for this 4% performance increase. And there is no reason to assume it's required; table based exceptions are simply faster than Option/Either.
This is esp true because Option/Either are not a complete solution anyway, both Go and Rust have panics that are required to do precise stack unwindings and where destructors/defer statements have to be run. And both Go and Rust do not track "calls panic" in their type systems.
That was a good read. Do you have any more articles you can point me to that explain your design choice of using exceptions rather than monads or even fancier concepts? Is it largely the performance benefits you've argued for here?
I've found exceptions to be problematic from a spooky-action at a distance/coupling perspective, but I mostly write short-lived CLAs and server apps, where perf tuning is a late-stage concern.
Anyway, Nim is thoughtfully designed, so I'd like to better understand the tradeoffs you made.
Exceptions have been part of Nim from day one and since then have been tamed with .raises annotations and inference. There is no article justifying the design decision because back then it was state of the art. And it still is btw because the alternatives are worse.
Performance is not the reason why Nim has exceptions but it is true that a specialized language construct can give you some performance benefits because optimization is specialization.
I've found exceptions to be problematic from a spooky-action at a distance/coupling perspective, but I mostly write short-lived CLAs and server apps, where perf tuning is a late-stage concern.
I've found the alternative solutions that people bring up to be much worse. Except for the "poison value" design pattern but that's currently not en vogue.
And I say this as somebody who hates exceptions because they are annoying to implement and to support.
That made me curious on your perspective on Result types ala Rust (I think Go has that idea as well but I haven't written a single line of code in it, so no clue).
During my brief stint with the language 2 years ago it seemed like a pretty neat idea, like enforcing annotating everything with Raises. Was the conclusion it was too much of a PITA, since it's sort of implicitly defining single-use object variants on the fly?
That made me curious on your perspective on Result types ala Rust (I think Go has that idea as well but I haven't written a single line of code in it, so no clue).
For attribution, Haskell's Maybe and Either type and C# popularizing them via their own Maybe Monad inspired Rust. And Rust's pattern matching is inspired by Haskell as well.
I personally have some issues with Nim exceptions:
- In the past exceptions were just incompatible with multithreading due to thread-local heaps.
- The reraised exceptions are unreadable in async. And the improved readability of stacktraces is the main draw of exceptions. And even for that, pretty sure we can use Result and a macro for emulating stack traces.
See also this 2018 discussion with the Nimbus team on error handling: