-
Notifications
You must be signed in to change notification settings - Fork 416
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
List-like union as struct to save allocation + virtual dispatch #472
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe the performance benefit has not been demonstrated to offset the more awkward code.
MoreLinq/ListLike.cs
Outdated
T this[int index] { get; } | ||
} | ||
readonly IList<T> _l; | ||
readonly IReadOnlyList<T> _r; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These member names are quite cryptic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tried to address this with 064fb61. Perhaps rw
and rx
are still cryptic, but wanted to keep them the same length (trying to hard?) so these are *nix-inspired (rw
= read-write, rx
= read-only).
@@ -21,34 +21,35 @@ namespace MoreLinq | |||
using System.Collections.Generic; | |||
|
|||
/// <summary> | |||
/// Represents an list-like (indexable) data structure. | |||
/// Represents a union over list types implementing either |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of a union, what about a delegating tuple?
static class ListLike
{
public static (Func<int> GetCount, Func<int, T> GetItem) => (() => list.Count, (i) => list[i]);
public static (Func<int> GetCount, Func<int, T> GetItem) => (() => list.Count, (i) => list[i]);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't syntactically correct, but I think I get your idea nonetheless. This would cause two closures and indirect calls so I have my doubts it's as optimal.
Conflicts resolved: - MoreLinq/ListLike.cs
Codecov Report
@@ Coverage Diff @@
## master #472 +/- ##
==========================================
- Coverage 92.39% 92.36% -0.04%
==========================================
Files 112 112
Lines 3434 3443 +9
Branches 1021 1023 +2
==========================================
+ Hits 3173 3180 +7
Misses 199 199
- Partials 62 64 +2
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
Benchmark code: [ParamsSource(nameof(Sources))]
public IEnumerable<int> Source { get; set; }
public IEnumerable<IEnumerable<int>> Sources
{
get
{
var xs = Enumerable.Range(1, 1_000_000);
yield return xs;
yield return xs.ToArray();
}
}
[Benchmark]
public void CountDown()
{
Source.CountDown(1, (_, _) => 0).Consume();
} Benchmarks for
Notice that the run with an array as a source is slower. Following is the benchmark for changes in this PR up to 064fb61:
The run with an array as a source is slightly faster. |
@atifaziz If you would, please compare your latest results to SuperLinq 4.6.0? I did a comparison against latest public release (3.3.2), and I show that SuperLinq is 40% faster on array and essentially equivalent (4% faster) on enumerables.
For
Data: https://gist.github.com/viceroypenguin/ec71e7adb8124c455e8a2925bee1b987 |
I had already removed
Data: https://gist.github.com/viceroypenguin/ec71e7adb8124c455e8a2925bee1b987 |
I shared those benchmarks to get back to the comment from @fsateler, where he rightly challenged that the performance & allocation benefits were not demonstrated (trade-off being code legibility). So this PR is not about diff --git a/MoreLinq/CountDown.cs b/MoreLinq/CountDown.cs
index f7b8ade1..1f5f72da 100644
--- a/MoreLinq/CountDown.cs
+++ b/MoreLinq/CountDown.cs
@@ -63,17 +63,13 @@ static partial class MoreEnumerable
? IterateCollection(collectionCount)
: IterateSequence();
- IEnumerable<TResult> IterateList(ListLike<T> list)
+ IEnumerable<TResult> IterateList(IListLike<T> list)
{
- var countdown = Math.Min(count, list.Count);
+ var listCount = list.Count;
+ var countdown = Math.Min(count, listCount);
- for (var i = 0; i < list.Count; i++)
- {
- var cd = list.Count - i <= count
- ? --countdown
- : (int?) null;
- yield return resultSelector(list[i], cd);
- }
+ for (var i = 0; i < listCount; i++)
+ yield return resultSelector(list[i], listCount - i <= count ? --countdown : null);
}
IEnumerable<TResult> IterateCollection(int i) then it makes a stark improvement:
In fact, it's almost 15% faster than SuperLinq (which I'm guessing is just iterating the list via the enumerator).
So perhaps not? I haven't investigated in detail as to why, but if I were to take a somewhat educated guess, then I'd say iterating a sequence makes two virtual dispatches per loop iteration ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@fsateler I'm going to assume you don't have the time to review, so I will go ahead and merge this since there's been at least another pair of eyes (@viceroypenguin) over it. If there's any strong objection (at some future point) to some aspect or the whole refactoring then feel free to open a follow-up issue. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had the review but I forgot to actually submit it :(
Nothing critical but here it is anyway.
This PR converts the list-like union via a class hierarchy to a
struct
for the purpose of saving GC allocations and additional virtual hops. I don't expect there will ever be more than one allocation saved per list and per invocation of an operator so the savings there are really pathetic. The implementation also feels a bit clumsy but there is not much choice there.