We see that the fastest C# version is 6 times slower than the rust/c++ implementation.
But that's super deceiving because those versions use arena allocators. Doing the same (wrote this morning, actually) yielded a ~20% difference vs the fastest rust implementation.
This was with dotnet 9.
I think the model of using GC by default and managing the memory when it's important is the sanest approach. Requiring everything to be manually managed seems like a waste of time. C# is perfect for managing memory when you need it only.
I like rust syntactically. I think C# is too object-oriented. But with a very solid standard lib, practical design, good tools, and speed when you need it, C# remains super underrated.
> The fact that Rico Mariani was able to do a literal translation of the original C++ version into C# and blow the socks off it is a testament to the power and performance of managed code. It took me several days of painful optimization to catch up, including one optimization that introduced a bug, and then Rico simply had to do a little tweaking with one hand tied behind his back to regain the lead. Sure, I eventually won but look at the cost of that victory
duncans · 2h ago
Unrelated, but Microsoft should be ashamed that most of the links in that blog no longer work.
It more or less tells you to unlearn all functional and OOP patterns for code that needs to be fast. Just use regular loops, structs and mutable variables.
PaulHoule · 5h ago
This kinda stuff brings languages like C# and Java closer to Rust in performance, thinking like the "borrow checker" it understands the scope of some objects and puts them on the stack and avoids garbage collection and allocation overhead.
It keeps the unsung benefit of garbage collection for "programming in the large" in which memory allocation is treated as a global concern independent of everything else instead of a global concern that has to be managed locally in every line of code.
Rust's strategy is problematic for code reuse just as C/C++'s strategy is problematic. Without garbage collection a library has to know how it fits into the memory allocation strategies of the application as a whole. In general a library doesn't know if the application still needs a buffer and the application doesn't know if the library needs it, but... the garbage collector does.
Sure you can "RC all the things" but then you might as well have a garbage collector.
Efficient memory allocation is part of a well written libraries API.
gpderetta · 5h ago
GC is problematic for cross-language foundational libraries though (unless they run on the same VM of course).
gwbas1c · 2h ago
Makes me wonder how easy/hard it is to write a library in Rust and expose it to other languages.
steveklabnik · 1h ago
Can be viewed as either, you expose a C ABI and then the other languages can use it, which is fundamentally not super hard but fiddly, which could be viewed as hard but some languages have nice libraries that exist to make it easier, which is of course easier.
PaulHoule · 4h ago
But what’s so bad about that Clojure and Java make a great team.
Grikbdl · 2h ago
Yeah but now try to make your Java library useful to a C#, Go or Python application.
PaulHoule · 2h ago
In the case of Python I think you could produce something like Jython that runs inside the Java runtime and lines up
with the Python FFA so you could run things like numpy inside of it. Would be transformative for my side projects.
SkiFire13 · 4h ago
> thinking like the "borrow checker" it understands the scope of some objects and puts them on the stack and avoids garbage collection and allocation overhead.
On the other side however if you don't write code that the borrow checker would accept you likely won't get these optimizations. And even if it was accepted there's a chance the analysis needs to be too deep or complex for the escape analysis to work. Ultimately this is a nice speed up in practice but not something I would rely on.
PaulHoule · 4h ago
The counter to that is that is that performance really matters in inner loops and in those cases the hot area is not that big, it doesn’t matter if your setup and tear down give up a lot of possible performance.
Early this year my son was playing chess which motivated me to write a chess program. I wrote one in Python really quickly that could challenge him. I was thinking of writing one I could bring to the chess club which would have had to have respected time control and with threads in Java this would have been a lot easier. I was able to get the inner loop to generate very little garbage in terms of move generation and search and evaluation with code that was only slightly stilted. To get decent play though I would have needed transposition table that were horribly slow using normal Java data structures but it could have been done off heap with something that would have looked nice on the outside but done it the way C would have done it in the inside.
I gave up because my son gave up on chess and started building and playing guitars in all his spare time.
Chess is a nice case of specialized programming where speed matters and it is branchy and not numeric.
ofrzeta · 3h ago
I guess you could to now try some realtime audio processing. Surprisingly (to me) there are actuall existing realtime DSP packages for Java.
PaulHoule · 2h ago
Yeah, audio DSP is not challenging which is what killed off the high-end soundcard.
So far my collab with him has been over physics and electronics such as scaling for the prototype electric octobase (one string so far) that he made. His current stretch project is: he cut a MIDI keyboard in half and stacked the segments on top of each other like an organ keyboard and he's attaching that to two electric guitar necks. Probably the MIDI is going to go to a rack-mount MIDI synth so it's going to have one hell of a wire bundle coming out of it.
which can take the sound from an electric guitar or bass and turn it into MIDI events or the equivalent that controls a synthesizer. Most commercial versions have six independent pickups, they used to be connected to the brains with a ribbon cable but some of them now digitize on the guitar and send the data to the controller over a serial link
This is wishful thinking. It’s the same as other layers we have like auto-vectorization where you don’t know if it’s working without performance analysis. The complexity compounds and reasoning about performance gets harder because the interactions get more complex with abstractions like these.
Also, the more I work with this stuff the more I think trying to avoid memory management is foolish. You end up having to think about it, even at the highest of levels like a React app. It takes some experience, but I’d rather just manage the memory myself and confront the issue from the start. It’s slower at first, but leads to better designs. And it’s simpler, you just have to do more work upfront.
Edit:
> Rust's strategy is problematic for code reuse just as C/C++'s strategy is problematic. Without garbage collection a library has to know how it fits into the memory allocation strategies of the application as a whole. In general a library doesn't know if the application still needs a buffer and the application doesn't know if the library needs it, but... the garbage collector does.
Should have noted that Zig solves this by making the convention be to pass an allocator in to any function that allocates. So the boundaries/responsibilities become very clear.
bob1029 · 4h ago
Use of a GC does not imply we are trying to avoid memory management or no longer have a say in how memory is utilized. Getting sweaty chasing around esoteric memory management strategies leads to poor designs, not good ones.
rudedogg · 4h ago
> Getting sweaty chasing around esoteric memory management strategies
I’m advocating learning about, and understanding a couple different allocation strategies and simplifying everything by doing away with the GC and minimizing the abstractions you need.
My guess is this stuff used to be harder, but it’s now much easier with the languages and knowledge we have available. Even for application development.
Arenas are fantastic when they work; when they don't, you're in a place that's neither simple nor particularly efficient.
Generational tracing garbage collectors automatically work in a manner similar to arenas (sometimes worse; sometimes better) in the young-gen, but they also automatically promote the non-arena-friendly objects to the old-gen. Modern GCs - which are constantly evolving at a pretty fast pace - use algorithms that reprensent a lot of expertise gathered in the memory management space that's hard to beat unless arenas fully solve your needs.
PaulHoule · 3h ago
For a lot of everyday programming arenas are "all you need".
pron · 3h ago
> It’s the same as other layers we have like auto-vectorization where you don’t know if it’s working without performance analysis. The complexity compounds and reasoning about performance gets harder because the interactions get more complex with abstractions like these.
Reasoning about performance is hard as it is, given nondeterministic optimisations by the CPU. Furthermore, a program that's optimal for one implementation of an Aarch64 architecture can be far from optimal for a different implementation of the same architecture. Because of that, reasoning deeply about micro-optimisations can be counterproductive, as your analysis today could be outdated tomorrow (or on a different vendor's chip). Full low-level control is helpful when you have full knowledge of the exact environment, including hardware details, and may be harmful otherwise.
What is meant by "performance" is also subjective. Improving average performance and improving worst-case performance are not the same thing. Also, improving the performance of the most efficient program possible and improving the performance of the program you are likely to write given your budget aren't the same thing.
For example, it may be the case that using a low-level language would yield a faster program given virtually unlimited resources, yet a higher-level language with less deterministic optimisation would yield a faster program if you have a more limited budget. Put another way, it may be cheaper to get to 100% of the maximal possible performance in language A, but cheaper to get to 97% with language B. If you don't need more than 97%, language B is the "faster language" from your perspective, as the programs you can actually afford to write will be faster.
> Also, the more I work with this stuff the more I think trying to avoid memory management is foolish.
It's not about avoiding thinking about memory management but about finding good memory management algorithms for your target definition of "good". Tracing garbage collectors offer a set of very attractive algorithms that aren't always easy to match (when it comes to throughput, at least, and in some situations even latency) and offer a knowb that allows you to trade footprint for speed. More manual memory management, as well as refcounting collectors often tend to miss the sweet spot, as they have a tendency for optimising for footprint over throughput. See this great talk about the RAM/CPU tradeoff - https://youtu.be/mLNFVNXbw7I from this year's ISMM (International Symposium on Memory Management); it focuses on tracing collectors, but the point applies to all memory management solutions.
> Should have noted that Zig solves this by making the convention be to pass an allocator in to any function that allocates. So the boundaries/responsibilities become very clear.
Yes, and arenas may give such usage patterns a similar CPU/RAM knob to tracing collectors, but this level of control isn't free. In the end you have to ask yourself if what you're gaining is worth the added effort.
rkagerer · 4h ago
Convention (as you report Zig does) seems to be a sensible way to deal with the problem.
> Also, the more I work with this stuff the more I think trying to avoid memory management is foolish ... It takes some experience, but I’d rather just manage the memory myself and confront the issue from the start.
Not sure why you're getting downvoted, this is a reasonable take on the matter.
qingcharles · 2h ago
The big benefit of this for my work is being able to run sites on smaller and cheaper boxes every year. It's not just the speed, but they've made huge strides in the last couple of versions in massively reducing the memory use of different object types.
This is only being compared to last year's v9, but if you compare against v7 from a couple of years ago, the changes are huge.
And this only reflects the changes to the underlying framework compilation, and doesn't factor in changes to say the Kestrel web server and static asset delivery that have taken a ton of load away.
Intel are also regularly checking in changes before they release new CPUs now so that the framework is ahead of their releases and takes advantage of new features.
jcmontx · 3h ago
Native AOT brings C# closer to Go, pretty nice feature.
BTW, Upgrading .NET apps for the last few years has been such a breeze. It won't take more than a few minutes + adjusting a couple build pipelines
yread · 7h ago
Amazing progress, some LINQ constructions were made 300 times faster. But reasoning over what code does heap allocations is getting more and more complicated. I guess intuition has to give even more way to benchmarking
kg · 5h ago
(Disclosure: I get paid to work on .NET)
The good thing is that the older techniques to minimize allocations still work, and tools like refs and ref structs make it easier to write zero-allocation code than it used to be.
But it's definitely harder to reason about whether optimizations will 'light up' for code that uses heap-allocated classes, closures, etc. than it was in the past, even if it's harder for a nice reason.
BenchmarkDotNet is a fantastic piece of tech at least, I have found it easier to benchmark in C# than in most other ecosystems I work with.
andix · 5h ago
Whenever I read about those yearly performance improvements, I wonder if there are some real world benchmarks. Some applications that were benchmarked on all .NET versions up from Framework 4.7 as a baseline. And all the .NET versions since as a comparison.
verdie-g · 4h ago
In my company running maybe 20K servers on .NET, we get a 10-20% CPU decrease every time we upgrade to the next major.
andix · 4h ago
That's impressive, considering a major release is sniped every year. I've never thought it would be that much of an improvement outside of synthetic benchmarks.
cjbgkagh · 3h ago
Managed devs have been begging for such fixes since the start, so many important perf gains were just left on the table at the same time we were being given the message that C++ and Javascript was the future. My exposure to it at MS was during the Win vs Dev Div conflict (the Steve Sinofsky / Steve Balmer era) and the message that managed software was slow was a core part of that battle.
This sort of post really makes me appreciate what's gone into the JVM. A lot of these optimizations are things that the JVM has long implemented. That's not a knock on C# either. Besides the JVM the only other place you'll see these sorts of optimizations are the likes of V8.
It makes me happy to see MS investing in C# like this. I love the notion of having competing VMed languages.
wiseowise · 3h ago
This whole post alone deserves a big kudos. Very detailed, true engineering culture.
Varelion · 5h ago
If only they could fix the ecosystem's stability; I feel like anything written with C#'s staple packages becomes outdated considerably faster than any other options.
homebrewer · 5h ago
TBH aspnet core has been the most stable web framework I've worked in my life. If you have good test coverage, upgrading projects between major versions often takes minutes — because nothing, or almost nothing, gets broken. Some things might get deprecated, but the old way keeps working for years, you can chip away at it slowly over the next year or two.
You still need to make sure that everything works, but that's what tests are for, and this has to be checked regardless of your tech stack.
Of course, they had a massive backwards compat break when moving from the regular aspnet to aspnet core, here's hoping nothing like that happens in the next 10-15 years..
bob1029 · 4h ago
I've hedged the stability risk by using the narrowest possible slice of the framework.
With enough experience you can accomplish pretty much everything using just the minimal API and router. HttpContext is the heart of AspNetCore. If you can get your hands on instances of it within the appropriate application context, you can do anything you need to. Everything else is dependent upon this. The chances that HttpContext can be screwed with are very, very low. There are billions of dollars riding on the fact that this type & API remains stable for the next decade+.
jtbaker · 4h ago
do you still use the framework's DI pattern with this approach? I have an older school .NET app I work on (still Core) sometimes and haven't gotten much experience with the minimal APIs, although it looks attractive as someone that prefers lower level routers.
bob1029 · 49m ago
I use the AddService() method to inject a things on occasion but its not used heavily. For lightweight apps I'll spin up things like a shared SQLiteConnection and inject it for all resources to use.
The DI pattern is simple & clean at this scale. In my top-level program I define my routes like:
The actual HandleRequest() method can do anything, including concerns like directly accepting and handling web socket connections.
tracker1 · 4h ago
I saw a lot of breaks between .Net Core 1, 2 and 3... since 3 it's been much more steady though.
Varelion · 5h ago
Or hope that key figures, or significant portions of the team don't get laid off. I don't trust Google or Microsoft products for this reason alone.
giancarlostoro · 4h ago
.NET has been powering StackOverflow for decades. All of .NET itself is MIT Licensed.
PaulHoule · 5h ago
I haven't written C# professionally since the early 2010s but back then the language had a big problem in that the old Container classes were not compatible with the Container<X> classes that were added when they added generics to C#. This created an ugly split in the ecosystem because if you were using Container<X> you could not pass it to an old API that expected a Container.
Java on the other hand had an implementation of generics that made Container<X> just a Container so you could mix your old containers with generic containers.
Now Java's approach used type erasure and had some limitations, but the C# incompatibility made me suffer every day, that's the cultural difference between Java and a lot of other languages.
It's funny because when I am coding Java and thinking just about Java I really enjoy the type system and rarely feel myself limited by type erasure and when I do I can unerase types easily by
- statically subclassing GenericType<X> to GenericType<ConcreteClass>
- dynamically by adding a type argument to the constructor
- mangling names (say you're writing out stubs to generate code to call a library, you can't use polymorphism to differentiate between
since after erasure the signature is the same so you just gotta grit your teeth and mangle the method names)
but whenever I spend some time coding hard in a language that doesn't erase generic parameters I come back and I am not in my comfortable Java groove and it hurts.
Rohansi · 4h ago
This hasn't been an issue for a long time because nobody uses the non-generic collections anymore. That doesn't help you with your reliance on type erasure though.
If you're up for it you should give it another try. Your example of subclassing GenericType<X> and GenericType<ConcreteClass> may be supported with covariance and contravariance in generics [1]. It's probably not very well known among C# developers (vs. basic generics) but it can make some use cases a lot easier.
Covariance and contravarience is very useful, but it's quite annoying that it can only be used with interfaces and not classes/structs. I write lots of little monoid-like data wrappers like Named<T>, and I have to either go through the hassle of dealing with interfaces for something that shouldn't need it, or I have to abandon covariance. The .NET maintainers seem to think that nobody wants covariance on classes, but it would be very helpful to me.
PaulHoule · 3h ago
Yeah, I had a chance a few months back when I was the backup programmer in a game development hackathon and my team was developing with Unity which uses C#. It was fun.
People talk about tradeoffs with GC, the worst one is that I've seen an occasional game that has a terrible GC pause, for instance Dome Keeper based on Godot which also runs in .NET. I used play a lot of PhyreEngine (also .NET) games on the Playstation Vita and never noticed GC pauses but I think those games did a gc on every frame instead of letting the garbage pile up.
uyjulian · 3h ago
PhyreEngine doesn't use .NET, and it is written in C++ (with optional Lua for scripting). You might be thinking of PlayStation Mobile, which does use .NET for scripting.
mrsmrtss · 4h ago
And yet, Java is trying desperately to fix its generics now with project Valhalla (although value types are their main goal), but it's almost impossible to do without major breaking changes. Without reified generics there's major performance cost when primitive (or value) types are used as generic parameters, because of boxing. Also, .NET has had generics since 2005 when .NET Framework 2.0 was released.
CyanLite2 · 5h ago
lol, whut?
Microsoft created the ".NET Standard" for this. Literally anything that targets .NET Standard 1.0 should work from circa 2001 through modern day 2025. You still get the (perf) benefits up the runtime upgrade which is what the blog post is about.
Rohansi · 4h ago
.NET Standard is more for maintaining interoperability between .NET Framework and .NET (Core). At this point only (very) legacy applications should be on Framework. Everything else isn't, so they shouldn't use. NET Standard as a target because it limits the new features they can use.
elyseum · 3h ago
I support your statement for custom dev applications.
Unfortunately some large enterprise applications like D365 still require the Framework :(.
giancarlostoro · 4h ago
Can you provide more examples? I've taken a Win32 application from .NET 3.5, converted it to a .NET Console Application (it was a server with a GUI) to run on .NET 8 with minimal friction, a lot of it wound up me just importing .NET Framework packages anew from NuGet.
What are you looking for out of .NET? The staple packages don't go away as often as in languages like NodeJS
9cb14c1ec0 · 3h ago
If you are not on the bleeding edge of whatever new framework Microsoft is promoting today, the ecosystem is incredibly stable.
porridgeraisin · 5h ago
Yep. A breaking change that makes my code a gajillion times faster is still always just a dirty breaking change that I'll hate.
Bs, basically, the docs about upgrading between frameworks and what works with what is actually pretty current and often disappears after some years. Especially anything about edge cases. Several upgrades also demands that you do the upgrade version by version. It is tedious work if you don't have a full understanding of the app. Nuget has also become a complete dependency hell. Today you often have to point out what to use to get your legacy apps to build with newer .NET versions. You can't just go with latest.
qingcharles · 3h ago
They almost never make any breaking changes these days. And when they do it's because of something that is absolutely unavoidable. I've never hit one.
sidkshatriya · 4h ago
Now if only they could bring their focus on performance to windows 11 as a whole.
It’s just shocking how much faster vanilla Linux is compared to vanilla windows 11.
Edit: by vanilla Linux I mean out of the box installation of your typical distribution e.g. Ubuntu without any explicit optimisation or tuning for performance
tracker1 · 4h ago
What is "vanilla Linux"? Ubuntu+Gnome, Mint+Cinnamon, Fedora+KDE, Arch+COSMIC ..?
Each distro, platform and desktop manager and related apps are relatively different, though all work pretty well on modern hardware. I'm currently running PopOS COSMIC alpha with the 6.16-4 kernel via mainline. It's been pretty good, though there have been many rough edges regarding keyboard navigation/support in the new apps.
kirici · 4h ago
There happens to be a distro called Vanilla that is Debian-based, atomic, integrates distrobox etc.
This reads like one of those recipe blogs where you first need to hear about great grandpappy's migration during the potato famine before you can get to the details on how to make cupcakes. First 5 paragraphs are just noise.
yread · 4h ago
To be fair there is like 500 paragraphs of content after it
hvb2 · 2h ago
Try to imagine the hours going into a post like this.
These posts are among the very best, digging into details explaining why things work and why changes were made.
Every time they get released I'm happy because no one killed it...
There are some benchmark games that I relied on in the past as a quick check and saw it as underwelming vs rust/c++.
For example:
https://benchmarksgame-team.pages.debian.net/benchmarksgame/...
We see that the fastest C# version is 6 times slower than the rust/c++ implementation.
But that's super deceiving because those versions use arena allocators. Doing the same (wrote this morning, actually) yielded a ~20% difference vs the fastest rust implementation.
This was with dotnet 9.
I think the model of using GC by default and managing the memory when it's important is the sanest approach. Requiring everything to be manually managed seems like a waste of time. C# is perfect for managing memory when you need it only.
I like rust syntactically. I think C# is too object-oriented. But with a very solid standard lib, practical design, good tools, and speed when you need it, C# remains super underrated.
> The fact that Rico Mariani was able to do a literal translation of the original C++ version into C# and blow the socks off it is a testament to the power and performance of managed code. It took me several days of painful optimization to catch up, including one optimization that introduced a bug, and then Rico simply had to do a little tweaking with one hand tied behind his back to regain the lead. Sure, I eventually won but look at the cost of that victory
It more or less tells you to unlearn all functional and OOP patterns for code that needs to be fast. Just use regular loops, structs and mutable variables.
It keeps the unsung benefit of garbage collection for "programming in the large" in which memory allocation is treated as a global concern independent of everything else instead of a global concern that has to be managed locally in every line of code.
Rust's strategy is problematic for code reuse just as C/C++'s strategy is problematic. Without garbage collection a library has to know how it fits into the memory allocation strategies of the application as a whole. In general a library doesn't know if the application still needs a buffer and the application doesn't know if the library needs it, but... the garbage collector does.
Sure you can "RC all the things" but then you might as well have a garbage collector.
In the Java world we are still waiting for
https://openjdk.org/projects/valhalla/
https://openjdk.org/jeps/454
with the Python FFA so you could run things like numpy inside of it. Would be transformative for my side projects.
On the other side however if you don't write code that the borrow checker would accept you likely won't get these optimizations. And even if it was accepted there's a chance the analysis needs to be too deep or complex for the escape analysis to work. Ultimately this is a nice speed up in practice but not something I would rely on.
Early this year my son was playing chess which motivated me to write a chess program. I wrote one in Python really quickly that could challenge him. I was thinking of writing one I could bring to the chess club which would have had to have respected time control and with threads in Java this would have been a lot easier. I was able to get the inner loop to generate very little garbage in terms of move generation and search and evaluation with code that was only slightly stilted. To get decent play though I would have needed transposition table that were horribly slow using normal Java data structures but it could have been done off heap with something that would have looked nice on the outside but done it the way C would have done it in the inside.
I gave up because my son gave up on chess and started building and playing guitars in all his spare time.
Chess is a nice case of specialized programming where speed matters and it is branchy and not numeric.
So far my collab with him has been over physics and electronics such as scaling for the prototype electric octobase (one string so far) that he made. His current stretch project is: he cut a MIDI keyboard in half and stacked the segments on top of each other like an organ keyboard and he's attaching that to two electric guitar necks. Probably the MIDI is going to go to a rack-mount MIDI synth so it's going to have one hell of a wire bundle coming out of it.
Personally I am really fascinated with
https://en.wikipedia.org/wiki/Guitar_synthesizer
which can take the sound from an electric guitar or bass and turn it into MIDI events or the equivalent that controls a synthesizer. Most commercial versions have six independent pickups, they used to be connected to the brains with a ribbon cable but some of them now digitize on the guitar and send the data to the controller over a serial link
https://www.boss.info/us/categories/guitar_synthesizers/seri...
Also, the more I work with this stuff the more I think trying to avoid memory management is foolish. You end up having to think about it, even at the highest of levels like a React app. It takes some experience, but I’d rather just manage the memory myself and confront the issue from the start. It’s slower at first, but leads to better designs. And it’s simpler, you just have to do more work upfront.
Edit:
> Rust's strategy is problematic for code reuse just as C/C++'s strategy is problematic. Without garbage collection a library has to know how it fits into the memory allocation strategies of the application as a whole. In general a library doesn't know if the application still needs a buffer and the application doesn't know if the library needs it, but... the garbage collector does.
Should have noted that Zig solves this by making the convention be to pass an allocator in to any function that allocates. So the boundaries/responsibilities become very clear.
I’m advocating learning about, and understanding a couple different allocation strategies and simplifying everything by doing away with the GC and minimizing the abstractions you need.
My guess is this stuff used to be harder, but it’s now much easier with the languages and knowledge we have available. Even for application development.
See https://www.rfleury.com/p/untangling-lifetimes-the-arena-all...
Generational tracing garbage collectors automatically work in a manner similar to arenas (sometimes worse; sometimes better) in the young-gen, but they also automatically promote the non-arena-friendly objects to the old-gen. Modern GCs - which are constantly evolving at a pretty fast pace - use algorithms that reprensent a lot of expertise gathered in the memory management space that's hard to beat unless arenas fully solve your needs.
Reasoning about performance is hard as it is, given nondeterministic optimisations by the CPU. Furthermore, a program that's optimal for one implementation of an Aarch64 architecture can be far from optimal for a different implementation of the same architecture. Because of that, reasoning deeply about micro-optimisations can be counterproductive, as your analysis today could be outdated tomorrow (or on a different vendor's chip). Full low-level control is helpful when you have full knowledge of the exact environment, including hardware details, and may be harmful otherwise.
What is meant by "performance" is also subjective. Improving average performance and improving worst-case performance are not the same thing. Also, improving the performance of the most efficient program possible and improving the performance of the program you are likely to write given your budget aren't the same thing.
For example, it may be the case that using a low-level language would yield a faster program given virtually unlimited resources, yet a higher-level language with less deterministic optimisation would yield a faster program if you have a more limited budget. Put another way, it may be cheaper to get to 100% of the maximal possible performance in language A, but cheaper to get to 97% with language B. If you don't need more than 97%, language B is the "faster language" from your perspective, as the programs you can actually afford to write will be faster.
> Also, the more I work with this stuff the more I think trying to avoid memory management is foolish.
It's not about avoiding thinking about memory management but about finding good memory management algorithms for your target definition of "good". Tracing garbage collectors offer a set of very attractive algorithms that aren't always easy to match (when it comes to throughput, at least, and in some situations even latency) and offer a knowb that allows you to trade footprint for speed. More manual memory management, as well as refcounting collectors often tend to miss the sweet spot, as they have a tendency for optimising for footprint over throughput. See this great talk about the RAM/CPU tradeoff - https://youtu.be/mLNFVNXbw7I from this year's ISMM (International Symposium on Memory Management); it focuses on tracing collectors, but the point applies to all memory management solutions.
> Should have noted that Zig solves this by making the convention be to pass an allocator in to any function that allocates. So the boundaries/responsibilities become very clear.
Yes, and arenas may give such usage patterns a similar CPU/RAM knob to tracing collectors, but this level of control isn't free. In the end you have to ask yourself if what you're gaining is worth the added effort.
> Also, the more I work with this stuff the more I think trying to avoid memory management is foolish ... It takes some experience, but I’d rather just manage the memory myself and confront the issue from the start.
Not sure why you're getting downvoted, this is a reasonable take on the matter.
This is only being compared to last year's v9, but if you compare against v7 from a couple of years ago, the changes are huge.
And this only reflects the changes to the underlying framework compilation, and doesn't factor in changes to say the Kestrel web server and static asset delivery that have taken a ton of load away.
Intel are also regularly checking in changes before they release new CPUs now so that the framework is ahead of their releases and takes advantage of new features.
The good thing is that the older techniques to minimize allocations still work, and tools like refs and ref structs make it easier to write zero-allocation code than it used to be.
But it's definitely harder to reason about whether optimizations will 'light up' for code that uses heap-allocated classes, closures, etc. than it was in the past, even if it's harder for a nice reason.
BenchmarkDotNet is a fantastic piece of tech at least, I have found it easier to benchmark in C# than in most other ecosystems I work with.
https://endjin.com/blog/2024/11/how-dotnet-9-boosted-ais-dot...
It makes me happy to see MS investing in C# like this. I love the notion of having competing VMed languages.
You still need to make sure that everything works, but that's what tests are for, and this has to be checked regardless of your tech stack.
Of course, they had a massive backwards compat break when moving from the regular aspnet to aspnet core, here's hoping nothing like that happens in the next 10-15 years..
With enough experience you can accomplish pretty much everything using just the minimal API and router. HttpContext is the heart of AspNetCore. If you can get your hands on instances of it within the appropriate application context, you can do anything you need to. Everything else is dependent upon this. The chances that HttpContext can be screwed with are very, very low. There are billions of dollars riding on the fact that this type & API remains stable for the next decade+.
The DI pattern is simple & clean at this scale. In my top-level program I define my routes like:
And then I have HandleRequest implementations like: The actual HandleRequest() method can do anything, including concerns like directly accepting and handling web socket connections.Java on the other hand had an implementation of generics that made Container<X> just a Container so you could mix your old containers with generic containers.
Now Java's approach used type erasure and had some limitations, but the C# incompatibility made me suffer every day, that's the cultural difference between Java and a lot of other languages.
It's funny because when I am coding Java and thinking just about Java I really enjoy the type system and rarely feel myself limited by type erasure and when I do I can unerase types easily by
- statically subclassing GenericType<X> to GenericType<ConcreteClass>
- dynamically by adding a type argument to the constructor
- mangling names (say you're writing out stubs to generate code to call a library, you can't use polymorphism to differentiate between
and since after erasure the signature is the same so you just gotta grit your teeth and mangle the method names)but whenever I spend some time coding hard in a language that doesn't erase generic parameters I come back and I am not in my comfortable Java groove and it hurts.
If you're up for it you should give it another try. Your example of subclassing GenericType<X> and GenericType<ConcreteClass> may be supported with covariance and contravariance in generics [1]. It's probably not very well known among C# developers (vs. basic generics) but it can make some use cases a lot easier.
[1] https://learn.microsoft.com/en-us/dotnet/standard/generics/c...
People talk about tradeoffs with GC, the worst one is that I've seen an occasional game that has a terrible GC pause, for instance Dome Keeper based on Godot which also runs in .NET. I used play a lot of PhyreEngine (also .NET) games on the Playstation Vita and never noticed GC pauses but I think those games did a gc on every frame instead of letting the garbage pile up.
Microsoft created the ".NET Standard" for this. Literally anything that targets .NET Standard 1.0 should work from circa 2001 through modern day 2025. You still get the (perf) benefits up the runtime upgrade which is what the blog post is about.
What are you looking for out of .NET? The staple packages don't go away as often as in languages like NodeJS
Scott Hanselman has a very short blog on how 20 year old code is upgraded to the latest .NET in just a few short minutes: https://www.hanselman.com/blog/upgrading-a-20-year-old-unive...
It’s just shocking how much faster vanilla Linux is compared to vanilla windows 11.
Edit: by vanilla Linux I mean out of the box installation of your typical distribution e.g. Ubuntu without any explicit optimisation or tuning for performance
Each distro, platform and desktop manager and related apps are relatively different, though all work pretty well on modern hardware. I'm currently running PopOS COSMIC alpha with the 6.16-4 kernel via mainline. It's been pretty good, though there have been many rough edges regarding keyboard navigation/support in the new apps.
https://vanillaos.org/
These posts are among the very best, digging into details explaining why things work and why changes were made.
Every time they get released I'm happy because no one killed it...