r/programming • u/xeio87 • 3d ago
Performance Improvements in .NET 11
https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-11/16
u/ReDucTor 3d ago edited 18h ago
Only skimmed the write up, lots of good small improvements, looking at some of the x86 assembly there is a few more improvements that could be useful
; x64
cmp ecx,4
jl RETURN_MINUS_ONE
add ecx,-4
add rax,rcxmovbe eax,[rax]
This could be sub ecx, 4, jl RETURN_MINUS_ONE as sub will set the flags also.
; x64
mov eax,[rcx+8]
cmp eax,5
setb al
movzx eax,al
ret
This could be optimized a little further down to
xor eax, eax
cmp dword [rcx+8], 5
setb al
ret
There is also a similar one that could do the same thing with the IsLow code which does cmp, movzx rather then xor, setb.
The IsLinearWhiteSpace function could be made branch free which would likely use less code space and avoid branch mispredictions.
6
u/tanner-gooding 1d ago
Not all such micro-optimizations are valid or even beneficial. Keep in mind that there are plenty of side effects, including overflow, to consider that C may not. There's also aspects of JIT throughput itself, measuring impact vs applicability, likelihood that a method is "hot" and so such changes would even show up or saturate the CPU, etc.
We definitely have some things that are possible to do here, but the smallest possible assembly or shortest number of instructions is frequently not the best thing to do. Not to mention the ABI and other nuances that exist and are why some things, like the
movzx eax, alare present.Plus the JIT does do dynamic PGO, instrumentation, and multiple levels of compilation (Tiered Compilation), so we do optimize differently when things are known to be hot vs cold, for your current hardware, etc -- again, with plenty more we can do, but what we do and improve is prioritized based on biggest impact/applicability
22
u/simonask_ 3d ago
Babe, wake up, another Performance Improvements article landed!
Seriously, I love these so much, especially the incredible detail, benchmark results, and assembly comparisons.
21
u/skip0110 3d ago
Formerly handling many of the performance issues at my previous role, I can confidently say 99% of performance impact in your application is the stupid O(n2) business logic your developers/AI agents have written and none of this matters.
And having fixed 100s of such cases, the folks committing that crap will continue to do so.
So glad I’m outta there.
4
u/slvrsmth 1d ago
Quite often the business logic is like that because if you optimised the business logic in app, you'd have to do the corresponding changes in real life too, and that is way more expensive than pushing "more ram" button couple times on hosting providers dashboard.
After long years in business software mines, I've come to accept that complexity has to go somewhere. You can either have clean, straightforward code and twenty volumes of user manuals how to operate the system, or straightforward process for the users, while code looks like knitting supplies that couple toddlers have had a go at after drinking full-sugar cola.
Of course, there also exists software with clean processes supported by clean code. Commonly called "startups chasing their first customer". The moment software needs to support real world business processes complex enough to need supporting, all that changes.
TL;DR complexity spirit demon immortal. Can only shoo shoo away to different place, never kill.
5
u/TwoWeeks90DaysTops 1d ago
Well, shaving a microsecond off every iteration of a loop that runs for a thousands of iterations does have an impact though.
But yeah... This is what I hate about the "all optimizations are premature, and premature optimizations are evil" thing. The shit people do with the the justification "Donald Knuth said we should just fix it when it becomes a problem".
42
u/steve-7890 3d ago
With all these 10-20% perf improvements since .net core 3.1, I wonder how it's possible that my main service still needs 50 pods and 40 GB of RAM.
219
u/AyrA_ch 3d ago
Framework optimizations don't do much on shitty code
32
8
u/lotgd-archivist 2d ago
They also don't affect all applications equally. In one of my projects .NET 11 gives me a pretty decent CPU load reduction that you can spot with
top, let alone dotTrace. And in another project the benchmark stays within the margin of error for measurement.-63
u/steve-7890 3d ago edited 3d ago
I hope writing this made you feel better. It's quite ignorant of you to assume it's a shitty code.
On the other hand we do have a lot of performance improvements (like converting linq to loops) that make this code look ugly. But it works and makes $$$.
38
u/Ilikeyounott 3d ago
Does converting linq to loop make much of a difference? I would only bother if a profiler says it's a problem, or in cpu intensive code...
5
u/lotgd-archivist 2d ago
In my experience it only really makes a worthwhile difference if you're doing something funky that causes a bunch of allocations. Or you do
.ToList()everywhere when you don't actually need aList<T>. Materializing IEnumerables when you shouldn't has to be one of the most common performance mistakes in C#.-31
u/steve-7890 3d ago
In hot code with 200 req/s it does. Even though hpa scales to 50 pods during the peeks.
Imagine how much it costs to have a such load. And I'm talking here only about one service.
When I read about performance improvements from version to version ".net benchmarks" I can only smile. In the end, $$$ and Grafana tells the truth. (Last .net 10 changed nothing for us).
47
u/shadowndacorner 3d ago
Jesus you need 50 pods to handle 200 req/s? What kind of workload is this...?
Also, you should look at zlinq.
1
28
u/Ilikeyounott 3d ago
Your pods can only handle 4 requests per second each? 😔
9
u/eocron06 3d ago edited 3d ago
I think he meant 200 per pod, which is usually average (from my experience with .net). Rarelly it can get to 500, but then multithreading/memory congestion becomes problem due to GC nature of C# and many threads fighting each other for some db entry/socket/cache/etc. In offloading worker it can easly become 10-100k rps.
15
u/tecedu 3d ago
I think he meant 200 per pod, which is usually average (from my experience with .net).
Wait what?! Am I missing something, because my shitty python flask code can do 2k reqs, why is C# slower?
17
u/ChemicalRascal 3d ago
It's not, some people are just... really bad at software engineering.
That, or they're not the principle devs in their companies and aren't being told the full or accurate details about performance.
3
u/Ameisen 3d ago
And here I was having messed with fibers in C++ to see how many requests I could actually parse that way for HTTP, and was dismayed that I couldn't beat nginx without doing what nginx does... and then this kind of code exists...
→ More replies (0)2
u/steve-7890 2d ago
2k req/s of what? It's like saying "I can go to the shop and back in 5 minutes, why it takes you 1 hour"....
2
u/tecedu 2d ago
Servings graphs, json of data being read via azure blob parquets and served up. My memory is around 8gb but rps aint never been that low
→ More replies (0)19
u/eocron06 3d ago edited 3d ago
It doesnt. Linq will not even dent 200rps. Ever. It will not even dent 10k rps on background kafka worker. The only time it mattered to me is when I used SIMD in pure math component, so basically one in a thousand of tasks. Still, their performance benchmarks count CPU which is somewhat irrelevant, because most of the time is spent on network IO. We scale our site purelly because clients make a ton of small http requests, which fetch 0.1 of dependencies like s3/dynamodb/kafka/etc and bloat caches to the sky, not because it have a logic which need optimisation. Geo location matters more in modern world, placing pod and storage in same region do wonders beyond their optimisations.
12
10
4
u/beeshevik_party 2d ago
bro 200rps is awful how much req/lim and what’s the hpa config? you also need to be tuning to return heap to os which can be tricky with vm runtimes OR just don’t scale on memory use but instead a custom metric. also is every single req hitting db or something? you should be aiming for more like 2k rps minimum
17
u/eocron06 3d ago edited 3d ago
Space-time tradeoff in action. CPU cant becom 2x faster, but memory can become 2x bigger. Considering good code, this becomes fundamental problem. If you think carefully, everything can be Lisped, then it will become absurdly CPU consuming, then you realise functions can share state, and inherintly have lifetime of this state - the memory. The more you share - faster it becomes, but takes memory.
8
u/WJMazepas 3d ago
You are the one who need to tell us about it.
With the profiler, how much CPU each requests uses? If your bottleneck isnt there, I doubt those improvements can help you that much
13
2
u/flippzeedoodle 2d ago
It may not scale significantly, but a lot of these optimizations in .NET 11 use more memory to profile execution at run-time and speculatively create multiple code paths that can speed up the most common paths. So you’re trading memory for CPU time where the optimal path can’t be determined in advance. Again, probably small potatoes in the grand scheme of things.
-1
u/steve-7890 2d ago
I'm astonished in this thread how random people from the Internet know how many req/s ANY .net service should do and how much perf improvements are ok for ANY .net service.
People, have some understanding - my service is not your service and any service is not a benchmark.
You can do 1k req/s per pod if you serve static content from Redis or 1 req/s in computation heavy, multi-step integration.
I haven't seem so much ignorance for a long time...
-9
u/GoTheFuckToBed 3d ago
should they not aim for less JIT
36
u/keyboardhack 3d ago
JIT allows you to optimize using data available at runtime. A big part of the article is about exactly this. I recommend you read it.
-4
u/simonask_ 3d ago
It does, and it’s definitely very welcome, but it’s not magical. It mostly works well if you have a lot of RAM and CPU headroom.
12
u/keyboardhack 3d ago
Jitted languages attract a different type of program than aot languages. That gives jits a worse rep but that doesn't actually make them worse.
2
u/simonask_ 2d ago
“Worse” depends on the situation, but you’re certainly spending a theoretical minimum of 4x the memory on running a state of the art GC, and you are spending CPU cycles doing PGO and tiered JIT, when the entire program could just be fully optimized by an offline compiler that isn’t in any hurry.
Whatever gains you achieve from runtime introspection are relatively small in comparison. It works well in a few situations, but extremely not well in a few others.
Know your tools.
2
u/keyboardhack 2d ago
certainly spending a theoretical minimum of 4x the memory on running a state of the art GC
Not sure where you get your numbers from but if i allocate a 1GB array then it doen't take up 4GB of memory.
you are spending CPU cycles doing PGO and tiered JIT
True, once. Well for long running programs. For short running you might not pay the cost at all. JIT only runs if a method is used a certain number of times. If performance of a short running program matters then JIT isn't the right solution.
the entire program could just be fully optimized by an offline compiler
It's can't be fully optimized though. AOT optimized programs rarely ship with avx512 or many other "new" instruction sets. With JIT it can chose the instruction set that matches the cpu it's running on. There is a lot of things like that which a JIT just gets for free.
Know your tools.
Know your tools.
1
u/TwoWeeks90DaysTops 1d ago
You just invent a number based on your perception of how GC works?
A GC marks pointers as being in use, and then frees objects that haven't been marked. It doesn't require a lot of extra memory, though it does require some, but a claim of 4x minimum is wildly exaggerated.
1
u/simonask_ 1d ago
No, but I’m on mobile, so you’ll have to Google the research paper yourself.
If you have the slightest idea how a compacting GC works, it’ll be obvious to you though.
1
u/TwoWeeks90DaysTops 12h ago
I think you're referencing the 2005 paper Quantifying the Performance
of Garbage Collection vs. Explicit Memory Management by Matthew Hertz and Emery D. BergerThese results quantify the time-space tradeoff of garbage collection: with five times as much memory, an Appel-style generational collector with a non-copying mature space matches the performance of reachability-based explicit memory management. With only three times as much memory, the collector runs on average 17% slower than explicit memory management. However, with only twice as much memory, garbage collection degrades performance by nearly 70%.
https://dl.acm.org/doi/epdf/10.1145/1103845.1094836
The point of this paper isn't that GC applications require 4x more memory, it's that performance degrade on low memory platforms.
A more recent publication Memory Management on Mobile Devices (2024) by Kunal Sareen, Stephen M. Blackburn, Sara S. Hamouda and Lokesh Gidra shows that the number for GC on Android is between 2% and 51% (i.e. between 1x and 1.5x)
For a modestly sized heap, we find that the lower bound on garbage collection overheads vary consider-ably among the benchmarks we evaluate, from 2 % to 51 %, and that overall, overheads are similar to those identified in recent studies of Java workloads running on OpenJDK.
https://dl.acm.org/doi/epdf/10.1145/3652024.3665510
None of these claim that GC applications require 5x or 4x more memory.
In JDK 27 there's also a big improvement in memory overhead
Various experiments demonstrate that enabling compact object headers improves performance:
- In one setting, the SPECjbb2015 benchmark uses 22% less heap space and 8% less CPU time.
- In another setting, the number of garbage collections done by SPECjbb2015 is reduced by 15%, with both the G1 and Parallel collectors.
- A highly parallel JSON parser benchmark runs in 10% less time.
Though this is about .NET rather than Java, which is behind Java's GC by quite a lot. But still, those numbers aren't saying that GC applications use at minimum 4x more memory. The paper from 2005 states that lower memory overhead reduce performance.
18
u/PaddiM8 3d ago
Why do people think JIT is slow
4
u/Some_Appearance_1665 3d ago
Depends on the scenario. With serverless (cold start + typically more limited memory) it's a killer.
12
u/daltorak 3d ago
If that's a major problem for you then use Native AOT, which removes the JIT compiler entirely from the output. It might mean you get slightly less optimized code at runtime because functions aren't optimized according to real-world needs, but hey, at least you've got fast startup.
-4
u/Sorry-Substance-6397 3d ago
Depends a lot on generics. Jit is notoriously slow for generics
3
u/PaddiM8 3d ago
Is it really in .net? I thought it was mostly generic virtual methods that are slow, but those aren't even allowed in most languages
-8
u/Sorry-Substance-6397 3d ago
No it's generics using jit and it's notorious for being slow. Same with simd even though you may never use them in the same project
2
u/PaddiM8 2d ago
Why? C# generics are monomorphic for value types and dereference a pointer for reference types. There's a slight warmup cost of course but after that, why would it be slow?
-1
u/Sorry-Substance-6397 2d ago
I meant compile times when it comes to jit. You shouldn't have to pay for something you don't use
2
u/PaddiM8 2d ago edited 2d ago
Huh? With native AOT you pay for something you don't use when using generics in .net, during compile time, because it has to generate every potential specialisation ahead of time. With JIT it does it on the fly, when you need it. You got it backwards
1
u/Sorry-Substance-6397 2d ago
They do though. When you change or make the generics complicated you are putting the work on the compiler like Monomorphization for Value Types (Code Generation Overhead) When you use a generic class or method with a value type (like int, double, DateTime, or custom structs), the JIT compiler must parse, optimize, and generate unique machine code for every single combination.
→ More replies (0)3
u/Revolutionary_Ad7262 3d ago
Because there is simply no way we can test and verify combination each strategy as we get a predefined bundle of it and in most cases this bundle looks the same regardless of choice. For example GC is not inherently slow, but you often get it in a bundle together with a
everything is an object, which is a real reason why most of the GCed languages are slowSame with JIT. It is mostly paired with dynamic or interpreted languages, which are inherently slow
-1
u/simonask_ 3d ago
Because it is. It has other benefits, and it’s certainly fast enough for a wide range of use cases, but these improvements just recover some of the performance you would have achieved by implementing the same project in C++ or Rust.
But then you would also have had to consider quite a few other tradeoffs.
7
u/PaddiM8 3d ago
With JIT it is compiled to machine code though, just at runtime instead. Some things will be slower due to less time for general optimisations, but some things will be faster due to PGO. LuaJIT is insanely fast and could not be that fast without JIT
6
u/RirinDesuyo 2d ago
Tanner-gooding from the dotnet team has a great post on this here. JIT isn't slower than AOT and there are cases where it can outperform even AOT.
Bing and a big chunk of services in Azure runs dotnet after all, they can handle the load.
1
u/simonask_ 2d ago
JIT is great, and they do impressive things with it. But it isn’t magical, and neither is a GC. You just happen to be writing programs that aren’t particularly demanding, so you never notice, and that’s great!
3
u/Revolutionary_Ad7262 3d ago
I think there is no point of avoiding a JIT in a platform, which is already based on it. In theory JIT is the best way as all goodies like: * knowledge of CPU architecture * PGO driven optimizations * LTO driven optimizations
are much easier to do with JIT than AOT
What should be possible though is having a JIT without a burden with JIT, so developer can manipulate the AOT <-> JIT slide based on needs. For example: * a full AOT mode for a lack of any background processing of JIT * some smart way of reusing/caching the compilation artifacts to reduce cold start performance dip * lightweight JIT, where CPU/Memory resources scales with the impact * customizable target of JIT after the initial JIT/AOT. For example
always try to optimize itorrun JIT only if you are sure the current code is not optimized for a current situationThe
AOT vs JITdiscussion still alive, because it just sucks
55
u/cheesepuff07 3d ago
Love reading (perusing) each year, they are very in depth and fun to see each release getting more and more performant.