Hey, thumbs up for all the effort. I would like to point out that LINQ is designed for readability, not performance, and it's excellent in situations where you need to apply complex transformations over not very big collections. Optimizing where optimizations are not need is, in the best case, harmless.
True, although one can have it's cake and eaten with LINQ by improving some of the operators by using structs, value delegates, de virtualisation, better branching code etc. My next couple of videos in the series show how to achieve this with LINQ, plus there are frameworks that do this out of the box as well: ♦ github.com/NetFabric/NetFabric.Hyperlinq ♦ github.com/reegeek/StructLinq
@@LevelUppp Yup, certainly and there's nothing wrong with that. My argument is mostly aimed at beginners when they fall into the trap of feeling they MUST optimize or they are doing it wrong.
Honestly, the best LINQ performance tip is to not use LINQ. In many cases it's a compromise between readability vs performance, choosing between LINQ and a custom loop that does exactly what you need. There are, of course, cases where LINQ provides nice additions (e.g. null-checks) without doing anything too crazy on top of that (like .Any()). Just be mindful when using methods like GroupBy() which can be up to 9 times slower than a simple foreach.
Watch the other videos :) you can have your cake an eat it too. It requires some work but you can get LINQ at zero cost, but not out of the box you have to improve and re-implement methods and operators.
Filter_ToLower_04 has another difference from the first three: It doesn't return the result. So it doesn't have to copy the reference back to/off the stack, which means less memory access, etc. I'm pretty sure that's why it's faster than version 03, and not because of the change in comparison order. Though you're still correct that the number of comparisons in the respective lists will have an impact on a more elaborate app.
While pushing and pooping the stack does have an impact, it's incredibly fast, in this specific case it was the branch order since the data is skewed in such a way that the branch order does make an impact. That missing return was an accidental omission on my part and I've fixed it later.
At 11:50 where you talk about LINQ's Last() method, you say that LINQ doesn't check to see if the enumerable is actually a list. That's incorrect. LINQ does that actually: github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs#L1091 And in the next line, you can see it actually calls the index: if (count > 0) return list[count - 1]; github.com/microsoft/referencesource/blob/master/System.Core/System/Linq/Enumerable.cs#L1093 More importantly, your code works faster also because you don't check like LINQ to see if the list has any elements in it before accessing the index. This isn't the main cause why your code is faster, but it's important to note. The performance penalties you're seeing are of different reasons, not because LINQ doesn't check if the instance is a list
Yeah the old version of LINQ didn't do that check. The new version indeed does the check. The type check is what's slowing it down, and since it's using the interface version all calls are considered VCalls and it's another tiny penalty that is relevant at scale. Thanks ❤️
Don't get me wrong, your content is OK, LINQ is slower but some of the reasons you say why LINQ is slower not all of them are correct. Keep up the good work and thanks
@@danflemming3553 Hey, I'm not bothered or anything like that. I think you're doing great work correcting all of the mistakes that I've made. This will lead to better quality videos, more discussion, small arguments, corrections and everyone will benefit at the end. Thanks :)
@@LevelUppp Wow, great attitude dude 👍🏻 Many don't understand that and would be offended! It's exactly my point, the idea is to improve, we all need to learn and improve. I have a lot of things I need to improve myself and your videos are great!
@@LevelUppp Actually Last() works like that at least since 2014, that's at least 6 years ago 🙂: github.com/microsoft/referencesource/blob/9da503f9ef21e8d1f2905c78d4e3e5cbb3d6f85a/System.Core/System/Linq/Enumerable.cs#L967
Hello, I might be wrong but your example with GroupBy_O1 might be totally incorrect. By using your group by LINQ query you only building expression tree which is not materialised anywhere (you are not calling .ToList() after query or just .Count()) so your mesurement only shows how long it takes to build the LINQ expression for the runtime. I is never executed. I tried that example myself and with .ToList() call it takes almost 9 times more time than withou it. I think it might be a mistake on your side because you used .ToList() call in examples with filtering
And here I was thinking that there's a saving grace for LINQ but no :( although it's sill not that bad in performance numbers but my Lookup absolutely destroys LINQ.
When trying to do ECS in c# some time ago, I hit a wall when trying to provide a nice way of enumerating quickly throught various data buckets in an easy way. Mainly because C# can't inline lambdas. (Yet??). For instance: void IterateOnDataBuckets_Slow(Action iterator) { for (...) { // Imagine a more complex iterator. iterator(someData[i]); } } interface DataBucketIterator { void Evaluate(ref Data data); } void IterateOnDataBuckets_SillSlow(DataBucketIterator iterator) { for (...) { // Imagine a more complex iterator. iterator.Evaluate(ref someData[i]); // still quite slow. } } void IterateOnDataBuckets_FastAndInlineable(T iterator) where T : DataBucketIterator { for (...) { // Imagine a more complex iterator. iterator.Evaluate(someData[i]); // Since it's know what T is, it's able to inline this function. } } But of course, having to write a type just to enumearte the data, so I stopped the ECS experiement there. Wondering if you have any experience with that sort of problem?
Lambdas that need to capture context usually cannot be inlined since the compiler will create a class and push all of the captured variables there, thus inlining is out of the question. This is a current limitation of the compiler since in a lot of cases such treatment shouldn't be needed. Then there's a general problem that delegates cannot be inlined at the calle site yet another limitation that could be improved. What I would do is a struct that describes the iterator this can be created with an lambda function but it returns a struct that describes what needs to be done, this would let you solve the problem, the matching engine would be a bit more complicated (and perhaps limited) but you would see performance gains.
If you need to iterate quickly you should pay the most attention to the data layout of your components, inlining while help should not be the primary cost. Are you using SOA for components? Have you tried doing SOA with SIMD? For well crafted components and systems using the SOA over SIMD is usually very fast.
Sorry one last question :) What C# compiler are you using? If it's the Unity compiler things are a bit different there, and if it's the Burst Compiler thins are even more extreme and with the C#9 on the horizon you could try using function pointers but the Unity toolchain is even faster when using SIMD.
@@LevelUppp It was mostly an experiment to see how close I could get to c/c++ performance. I've not tried and use SIMD before really, but I've been wanting to since seeing your videos.
@@LevelUppp Yeah, it's really cool how fast they've gotten things with burst. They also inline the delegates, (but I have had some bugs with scope and capturing variables^^). Okay, I was not aware of C# 9 function pointers, that is awesome!
The problem with that is that if you're around ~5% better it would be good to do a statisticalsignificance test to be sure that your gains are not just random noise.
LINQ query syntax is very ugly to read, the method chain version is much more readable and modern in my opinion: transactions.Where(x => ...).Selec(x => x...);
For the ToLower bit: I am missing the option of first uniting the collections to be filtered by into a single one. That should also limit the number of ToLower calls to 1.
Great video. I would only recommend to work on a proper naming convention, as you named all of the tested methods with quite generic (nothing saying) names.
in groupByLookup you dont deal with collisions,and when i see the imp of groupBy provided by Microsoft it look very similar to yours , so why the performance gap is hug ?!
With the let example I wonder what the performance would be like using LINQ method syntax i.e. transactions.Where(t => { var nameLowerCase = t.Name.ToLower(); if.... }).Select(...
Too slow to measure. It takes an average of ~1-5 minutes per run. However new videos include Benchmark.NET as well either by having it already pre-computed or by speeding up the video 15x times when the benchmark runs.