-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Improve List<T>.AddRange performance for enumerables #76043
Conversation
AddRange is currently implemented as delegating to InsertRange, and InsertRange in turn has a more complicated inner loop as part of adding each item from a source enumerable into the list. By just copying InsertRange's source into AddRange, deleting all the irrelevant stuff, and changing the Insert call to Add, throughput improves measurably.
Tagging subscribers to this area: @dotnet/area-system-collections Issue DetailsAddRange is currently implemented as delegating to InsertRange, and InsertRange in turn has a more complicated inner loop as part of adding each item from a source enumerable into the list. By just copying InsertRange's source into AddRange, deleting all the irrelevant stuff, and changing the Insert call to Add, throughput improves measurably.
private List<int> _list = new List<int>();
private IEnumerable<int> _sourceEnumerable;
[Params(2, 256, 1024)]
public int Count { get; set; }
[GlobalSetup]
public void Setup()
{
_sourceEnumerable = Enumerable.Range(0, Count);
}
[Benchmark]
public void AddRange_Enumerable()
{
_list.Clear();
_list.AddRange(_sourceEnumerable);
}
|
The benchmark code makes me wonder, why |
It could; the question would be what scenario does that really serve, other than messing up my benchmarks :) It'd be more interesting I think to look at whether any of the other internal enumerable implementations in LINQ could/should implement |
I don't think it works. The most valuable ones would be on the iterators used for things like Select (it's really rare, for example, to see an Enumerable.Range used in production where the result is consumed directly; it's much more common to see a Select or the like off of it). The problem, however, is we'd need to run the selectors as part of |
This comment was marked as off-topic.
This comment was marked as off-topic.
Grow(_size + count); | ||
} | ||
|
||
c.CopyTo(_items, _size); |
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'm guessing this could regress cases where we're appending the list to itself?
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.
Regress functionally? We have this test:
runtime/src/libraries/System.Collections/tests/Generic/List/List.Generic.Tests.AddRange.cs
Line 50 in 9b9b0c9
public void AddRange_AddSelfAsEnumerable_ThrowsExceptionWhenNotEmpty() |
Is there a case that's missing?
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.
Functionality looks good, I meant to say regress performance.
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 don't believe so. I just tried with:
[Benchmark]
public List<int> AddToSelf()
{
var list = new List<int>();
list.Add(1);
list.Add(2);
list.AddRange(list);
list.AddRange(list);
list.AddRange(list);
list.AddRange(list);
list.AddRange(list);
list.AddRange(list);
return list;
}
and depending on the run there was either no difference or a small difference in favor in of the new version.
Is there a specific aspect of the change you think would regress that? I can try to address it if so. Though I'm not particularly concerned about the performance of that case, anyway.
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.
Assuming we cared about optimizing that particular case, hardcoding the operation in the style of InsertRange
might be marginally faster compared to using ICollection<T>.CopyTo
, at the expense of added branching for every other case. But like you said, it doesn't seem important enough to justify.
I've unlocked it. Thanks. |
AddRange is currently implemented as delegating to InsertRange, and InsertRange in turn has a more complicated inner loop as part of adding each item from a source enumerable into the list. By just copying InsertRange's source into AddRange, deleting all the irrelevant stuff, and changing the Insert call to Add, throughput improves measurably.