5 ms·
It's that in part. Here are some of my additional observations and or guesses. They invested a lot of time adding language features with compiler and runtime s
by rndgermandude 5y ago
It's that in part. Here are some of my additional observations and or guesses.
They invested a lot of time adding language features with compiler and runtime support to avoid e.g. heap allocations/copying, like Span<> and friends, (readonly) ref structs, in/ref/out parameters (ref and out parameters existed before but were used a lot less in the runtime), or ValueTasks to some degree. This in turn enabled a lot of potential for optimizations in the compiler (aside from essentially writing an entirely new bytecode compiler with Roslyn and entirely new JIT with RyuJIT, throwing out the crufty old compilers), in the general runtime, and in the specific runtimes/frameworks e.g. ASP.NET. Those optimizations have to be implemented first however, and more and more get implemented with each new version.
I have a project I maintain that sees an almost 50% speedup from net48 to net5, and another 10-15% speedup from net5 to net6 (based on the time it takes to run the extensive test suite). It isn't even that compute heavy. From profiling it appears that a lot of these speedups are due to internal copies of data being avoided, and a lot of additional fast-paths in the runtime (e.g. fast-paths for byte-arrays or character-arrays as opposed to taking the generic array slow paths).
Another thing of note is that they added a lot of `bool Try*(..., out result)`-style APIs meant to avoid exceptions and the associated handling, and switched a lot of internal code to use these functions. E.g. in the reference source of the net48 runtime I think there are still a lot of instances of
try {
var number = int.Parse(value);
}
catch {
// slow path/error path
}
instead of the new-idiomatic .netcore and later style of
if (!int.TryParse(value, out var number)) {
// slow path/error path
}
try-catch was/is slow-ish, and throwing exceptions is too, aside from it preventing inlining by the JIT a lot of times.
And while #nullable (source annotations for what is nullable or not) and associated annotations such as MaybeNullWhen() had no direct influence on how the compiler could optimize, it probably helped people a lot writing correct code and as a side effect a lot more code became compile-time provable non-nullable which enabled further optimizations e.g. generating code that skips redundant null checks.
- chrisoverzero 5y ago> the new-idiomatic .netcore and later style `Int32.TryParse` has existed since .NET Framework 2.0, which was released on February 13, 2002: https://docs.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netframework-2.0 https://docs.microsoft.com/en-us/dotnet/api/system.int32.try...
- rndgermandude 5y agoRight, this one has, a lot of other public or runtime-internal Try* methods have not. And even tho this particular one has existed for a long time, that doesn't mean it was used consistently in the runtime or in the popular first and third party frameworks. I'd argue the Try*-style, while artifacts of it were present before already, only really became widely idiomatic with dotnetcore.
- cyral 5y agoSurprising that in 2021 Java still doesn't have this in its standard library
- native_samples 5y agoWell, the Java approach is philosophically somewhat different. C#/.NET is closer to C++ where they are very willing to complicate the language and APIs to make the job of the runtime or compiler easier. Java just philosophically refuses to do that, more or less (perhaps you could argue that's changing a bit now with value types). So in Java they just made exceptions really fast. There are lots of runtime optimizations around exceptions, for example, if you regularly parse strings that aren't numbers then the resulting exception will automatically stop having its stack trace filled out, which makes throwing drastically cheaper. The JVM can also inline the code and then convert try/catches to ordinary gotos.
- CornCobs 5y agoHonestly I really like C#'s outvar and return success idiom. It's soooo ugly and yet slick at the same time. C does the same thing but I think the inline out var declarations make a huge difference to using them. Of course you miss out on error context an Exception or Result<T, Err> gives you but for many of the Try* functions it really doesn't matter.
- rndgermandude 5y agoYeah, I became a fan of that too. In a lot of cases Try* is the outright right approach, too. Like `IDictionary.TryGetValue(key, out var value)` is better than try { var value = IDictionary.GetValue(key); } catch (KeyNotFoundException) {}` and has no race like `if (IDictionary.ContainsKey(key)) IDictionary.GetValue(key);`. Try* functions are still free to throw in actually exceptional cases, just not on generic not-so-exceptional errors. If you really need context, there is nothing from stopping you from implementing Try* functions in your own APIs that either have another out param for the error information, return the error information instead of a bool or use a Result<T, Err> kind of type (or a tuple), either.
- ygra 5y agoIn the dictionary case, TryGetValue also saves you from having to hash and lookup twice unnecessarily (once for ContainsKey, and once for GetValue).
- jabits 5y agoThanks. This is an interesting comment…I had not realized coding styles in c# had changed in this way