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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
@@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.
@@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.
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)
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.
@@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.
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...
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.
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.
@@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...
@@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.
@@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
@@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.
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.
@@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.
@@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.