Тёмный

Here is How Fast is LINQ, Spans, and Everything 

Zoran Horvat
Подписаться 29 тыс.
Просмотров 11 тыс.
50% 1

Опубликовано:

 

11 сен 2024

Поделиться:

Ссылка:

Скачать:

Готовим ссылку...

Добавить в:

Мой плейлист
Посмотреть позже
Комментарии : 58   
@anm3037
@anm3037 8 месяцев назад
Feels good to see performance stuff on this channel
@protox4
@protox4 8 месяцев назад
Depending on which Linq queries are used, they can generates tons of waste. It's less of a concern with modern moving GC, but it matters very much in Unity where it's still using the old non-moving Boehm GC. I recently stripped Linq out of the project at work and replaced it with manual loops and pooled arrays, and the fragmented memory dropped by 25% from that alone! Fragmented memory makes the rest of the code run slower, not just the Linq queries themselves. This matters very much for frame rates and frame spikes from GC.
@zoran-horvat
@zoran-horvat 8 месяцев назад
Well, LINQ is generally not suitable for any realtime applications. Its focus is on consuming dynamic sequences of objects without collecting them. But your technical explanation of how Unity operates is very useful. Thank you.
@jongeduard
@jongeduard 7 месяцев назад
Unity is not regular DotNet, is always pretty far behind in versioning while it has other concerns too by being a game development platform and the all the specific additional things needed for that. It's really in very recent versions where the dotnet team added lots of low level CPU efficient stuff into the DotNet code base. The Sum function is exactly such a example where they did just that. Taking a look into it using the online Source Browser shows a TryGetSpan check followed by a call to a private implementation where they do explicit vectorization. I also know this because Nick Chapsas has talked about it in his videos. Technically you can write such code yourself, but it clearly goes a bit further than your typical for loop. I know that many compilers also vectorize code in their optimization process when producing a release build, but in DotNet this has to be either the JIT or the NativeAOT compiler and not the normal C# compiler which just generates IL. But apparently this still does not really win from explicitly written code that way.
@RupertBruce
@RupertBruce 7 месяцев назад
I wish my test cases were as clearly defined and have the same coverage as your benchmarks!
@mitar1283
@mitar1283 8 месяцев назад
Great video for real, people often talk about performance disregarding the actual optimizations that where made to Linq over the years. As we can see in this example, Linq has really come a long way.
@zoran-horvat
@zoran-horvat 8 месяцев назад
The second example, with strings, is indicative, as SelectMany operates on recursive enumerators, unlike the solution with nested loops. One should be aware of such differences that may incur a measurable delay. And even that only when the problem dimension warrants that you will be able to actually notice the delay.
@nickbarton3191
@nickbarton3191 8 месяцев назад
Ah the video I've been waiting for, great. And after watching it, you didn't disappoint. Thanks Zoran.
@nickbarton3191
@nickbarton3191 8 месяцев назад
Got a question, has the aggregate with lambda to sum got similar performance to list.sum which is also Linq? It's just a shortcut, right?
@dzllz
@dzllz 8 месяцев назад
Great video! Would have been interesting with a memory comparison on the last test also. But good points overall
@David-id6jw
@David-id6jw 8 месяцев назад
On the int test, you only showed the results for an array size of 10,000, so there is no feel for how things scale. With the string test, there was some interesting scaling effects. With N=100, there was only a slight penalty for using foreach vs span, but that grew with N=10000. Foreach went from 19% slower to 110% slower than the span solution. That's a fairly hefty penalty for a growing dataset, which hasn't even really gotten that large yet. With LINQ, the scaling penalty isn't nearly as bad. It went from 110% slower at N=100 to 160% slower at N=10000. It's still losing ground compared to the span implementation, but slowly enough that if the performance is adequate at low values of N, it should be relatively similar at high values of N. I suspect foreach will be slower than LINQ fairly soon if the dataset continues to grow. In both tests, you didn't show the memory allocation. I have suspicions about where things would go with memory, but suspicions aren't tests, and are notoriously unreliable. That's why we have tests in the first place.
@zoran-horvat
@zoran-horvat 8 месяцев назад
Times scale linearly with N, so you can make a rough estimate. The situation changes somewhat (but not too much) for small N, like 10-100. But those times are well under 1us, so I ignored them altogether.
@jongeduard
@jongeduard 7 месяцев назад
It's really important to consider the complete story. The general practice is to buffer things when you need to iterate over them more than one time. When I have both have a need for Count or Length while I also need to loop, I do a ToArray or ToList. If you forget that and iterate twice or more over the same lazy code, you're always going to face the worst performance possible. If you however can stay within a single iteration, you should not buffer. You can save not just CPU cycles, but especially memory as well. Being memory efficient is one of the main goals of LINQ. And higher order functions in general. Although Rust goes even further, where no GC exists and where heap usage is also extremely limited. The combinators are actualy zero cost and are faster than manual loops most of the time.
@zoran-horvat
@zoran-horvat 7 месяцев назад
I have made that point clear in several other videos. In this one, the focus was on other aspects and so I said that if speed is all you want, then don't turn a sequence into a collection - it will be slower. That assumes no other requirements but the speed.
@evancombs5159
@evancombs5159 8 месяцев назад
I will say, with cloud conputing often being usage based, optimizing for performance can have a real impact on the bottom line. That is changing the game when it comes to when to optimize.
@user-ic3vq1xi1p
@user-ic3vq1xi1p 6 месяцев назад
Many Thanks Great Man👍👍👍
@ml_serenity
@ml_serenity Месяц назад
Would also be nice to see the difference in memory allocations. In server scenarios excessive memory allocations can cause more performance issues than couple extra milliseconds to iterate through the collection. Spans and stack allocations can help with that a lot.
@zoran-horvat
@zoran-horvat Месяц назад
The benchmark results include garbage collection. And it is not milliseconds, but microseconds. Milliseconds is I/O.
@ml_serenity
@ml_serenity Месяц назад
@@zoran-horvat microseconds with 100 items? But what about 10th' of thousands?
@zoran-horvat
@zoran-horvat Месяц назад
@@ml_serenity Why not millions? Loading 100 items causes processing of 100 items, not 10,000. But if you planned to load 10,000 items, too, then scale the I/O time by the same factor. You will end up with the same picture, 99% of the time the CPU sitting idle and waiting for the data to pour in or for the data to pour away.
@ml_serenity
@ml_serenity Месяц назад
@@zoran-horvat What I mean is excessive memory allocations and how Spans help to reduce that drastically in server scenarios. Propagation of objects to gen 1 and gen 2 can be a real problem even today.
@zoran-horvat
@zoran-horvat Месяц назад
@@ml_serenity In 1% of applications, to be fair.
@HOSTRASOKYRA
@HOSTRASOKYRA 8 месяцев назад
Thank you very much!
@WillEhrendreich
@WillEhrendreich 8 месяцев назад
Ok, what if you wrote a native code way of doing that logic, in Odin for instance, then calling it from dotnet? What is the perf difference?
@yanivrubin7202
@yanivrubin7202 8 месяцев назад
Great video, but from my experience, GC has alot of effect on performance. And you didn't check how much memory each method wastes. In case of linq I guess it will be higher. And for the case that the linq code is being called many times, the linq method might be the most wasteful and eventually slower (on smaller collections)
@zoran-horvat
@zoran-horvat 8 месяцев назад
Why do you think that LINQ would "waste" memory? What objects does it create, and how many of them? Also, I don't think that it is your experience that GC has a lot of effect on performance, and I can tell you why. For any kind of objects used in a method, for GC to have a lot of effect, there must be a lot of those objects in the first place. That means that GC has already been measured in the benchmarks and it doesn't seem to me that that negative effect has shown up.
@yanivrubin7202
@yanivrubin7202 8 месяцев назад
@@zoran-horvat It doesn't create many objects. But it creates them each time linq is called. So in my case, we SADLY removed the linq code from hot functions (called several thousand times per second). The GC thanked us :-) And in it improved the program (but it was long time ago I need to retest it to see how many bytes) Regarding which objects, you can dive to linq code and see that it 'new' several objects. Mostly for it's internal management.
8 месяцев назад
Good video, shows that MS puts efforts in Linq. Could be more useful if .net version was mentioned.
@EugeneS88-RU
@EugeneS88-RU 5 месяцев назад
How about using pointers in for loop (unmanaged unsafe code)? I think it will be top performance
@ryanshaw45
@ryanshaw45 8 месяцев назад
Enjoyed the video! FYI - It looks like your discord link is broken
@zoran-horvat
@zoran-horvat 8 месяцев назад
I don't know what happens to those Discord links - they are supposed to never expire, and yet every now and then someone tells me it doesn't work anymore...
@C00l-Game-Dev
@C00l-Game-Dev 6 месяцев назад
Alternatively title for this video: How to make c# go brr
@chudchadanstud
@chudchadanstud 8 месяцев назад
Do you have a vid on your vs code setup? I always wanted to use vs code for c#
@zoran-horvat
@zoran-horvat 8 месяцев назад
I don't have such a video. Actually, I only started using VS Code consistently in my second or third attempt - I was so hooked up to VS. But now, after learning it well, I struggle when I must use VS again. VS is so bloated and slow compared to VS Code.
@egr0eg
@egr0eg 8 месяцев назад
What if you need to iterate over the collection multiple times? Is that a case when calling ToList is acceptable?
@zoran-horvat
@zoran-horvat 8 месяцев назад
Yes, it is. Actually, you should not be allowed to iterate an unknown IEnumerable multiple times, because some variants will fail if you do, and some will cost you evaluating the source twice. Unless you know precisely that the object at hand supports multiple iterations without adding prohibitive costs, you should collect the objects first. That opens another set of questions: how large is the data set you are processing. Sometimes, it is equally impossible to load an entire data source into a list either. That is where we turn to the theory of algorithms.
@matheosmattsson2811
@matheosmattsson2811 8 месяцев назад
@@zoran-horvat This partially answers my question above. But this gives me a follow up question: Why is it that Intellisense/ReSharper often suggest that my methods should return an IEnumerable instead of a T[]? I mean I kind of get it but at the same time it makes intellisense itself more stupid, as when I use the method and DO iterate the result multiple times, it will complain. Though, I know that the result is an ARRAY and that it should be OK, so I guess calling .ToArray() in this case is basically a "down cast" but feels a bit dumb...
@zoran-horvat
@zoran-horvat 8 месяцев назад
@@matheosmattsson2811 It is perfectly legal to return a collection from a method if that method's purpose is to construct it. Code tools may not understand the idea well, and they apply a general rule, which is also valid when there are no other constraints.
@matheosmattsson2811
@matheosmattsson2811 8 месяцев назад
@@zoran-horvat Okay, yea I agree. But so you're saying that you see no point in stating the return type is IEnumerable when you know yourself you are always returning a T[]? I at least would find it very misleading if a Class Library on NuGet would return an IEnumerable which in fact always would be a collection. It kind of makes the consumer doubt a bit on how it should be used, if I understand the concept correctly
@zoran-horvat
@zoran-horvat 8 месяцев назад
@@matheosmattsson2811 That is the classic omission in design. The correct decision is to return the least obligation, which is in this case IEnumetable. This conclusion comes from encapsulation - having a collection imposes costs and even limitations that may cause callers to fail, depending on the size of their call. Worse yet, the callers will lock the implementation into a decision to return that particular collection forever after. What if you discovered a more convenient, performant, efficient, or whatever other collection. Well, sorry, you won't be able to reimplement your method because the previous design has leaked out and now everybody depends on it. There is, then, that same special case, when the caller dictates a collection that makes it performant, or even is required to make the caller's operation feasible. In that case, that collection becomes a request, and the method must return it. In that case, the method becomes the factory of that collection. As you can see, that situation is quite different from the general case.
@Tymon0000
@Tymon0000 8 месяцев назад
For most of my cases in unity speed of iteration doesn't matter. What matters is how much garbage is created.
@zoran-horvat
@zoran-horvat 8 месяцев назад
When iterating many objects, most of the garbage are those objects themselves - the iteration method of choice makes little difference.
@defufna
@defufna 7 месяцев назад
if you are iterating over a list/array of objects that is already created with for loop there is no garbage, with foreach you have enumerator object as garbage.
@zoran-horvat
@zoran-horvat 7 месяцев назад
@@defufna First, the cost of the enumerator is amortized across multiple objects in the sequence. Second, if you use an array, then there is the array object itself to count in, and if it is a list, then there is the list on top of the underlying array, which counts as two objects. You cannot count the enumerator as garbage and then forget to count collections. Anyway, the collections still count as 1/N per object, when they contain N objects.
@defufna
@defufna 7 месяцев назад
@@zoran-horvat It is amortized, but you can have a case where you need to iterate over plenty of small arrays. Arrays and Lists can be long-lived, this is especially the case in games (op mentioned Unity) where you usually load everything at the start and try to avoid any new allocations during the game.
@zoran-horvat
@zoran-horvat 7 месяцев назад
@@defufna IEnumerable doesn't even apply to that case, so comparative analysis is not possible. IEnumerable does not warrant multiple iterations.
Далее
Avoid This Common Mistake in DDD Modeling
10:17
Просмотров 9 тыс.
Let C# Tuples Become Your Best Friends
11:51
Просмотров 10 тыс.
You May Have Never Learned This Lesson Right
13:02
Просмотров 11 тыс.
Liskov: The Liskov Substitution Principle
4:23
Просмотров 22 тыс.
17 Pieces of C# Syntax That Make Your Code Short
12:41
Why is C# Evolving This Way?
15:02
Просмотров 22 тыс.
The Ultimate Guide to C# Records
12:55
Просмотров 19 тыс.
Make Your LINQ Up to 10x Faster!
11:08
Просмотров 55 тыс.