-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Use the List(int) constructor if possible when lowering collection expressions #72427
Conversation
As far as I know SetCount is required for correctness, in order for the Span we get with AsSpan to have the correct length, and so on. In other words, if you want to adjust the codegen here, you should use the Also, some microbenchmark to demonstrate the change in perf characteristics may be warranted here, a la #71195 (comment) |
@RikkiGibson ah yep, I get it now. On a second pass I've reintroduced the call to |
Yeah there should be some examples of this in nearby lowering code. Look for existing usages of |
05c5f8e
to
96e6511
Compare
Microbenchmark:
[MemoryDiagnoser]
public class ListInitBenchmark
{
[Params(0, 1, 2, 3, 4, 10, 10000)]
public int N;
[Benchmark]
public List<int> CreateListNoCapacity()
{
List<int> list = new();
CollectionsMarshal.SetCount(list, N);
return list;
}
[Benchmark]
public List<int> CreateListWithCapacity()
{
var knownLength = N;
List<int> list = new(capacity: knownLength);
CollectionsMarshal.SetCount(list, knownLength);
return list;
}
} |
Let's include a test where the length calculation includes side-effects, so it's clear we're only calculating the length once. Perhaps: [CombinatorialData]
[Theory]
public void LengthWithSideEffects(
[CombinatorialValues(TargetFramework.Net70, TargetFramework.Net80)]
TargetFramework targetFramework)
{
string source = """
using System;
using System.Collections;
using System.Collections.Generic;
class MyCollection<T> : IEnumerable<T>
{
private List<T> _list = new();
public int Length
{
get { Console.Write("Length: {0}, ", _list.Count); return _list.Count; }
}
public void Add(T t) { _list.Add(t); }
IEnumerator<T> IEnumerable<T>.GetEnumerator() => _list.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
}
class Program
{
static void Main()
{
MyCollection<int> x = [1, 2];
MyCollection<object> y = [3];
List<object> z = [..x, ..y];
}
}
""";
CompileAndVerify(
source,
targetFramework: targetFramework,
verify: Verification.Skipped,
expectedOutput: IncludeExpectedOutput("Length: 2, Length: 1, "));
} Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:32933 in cdd8f75. [](commit_id = cdd8f75, deletion_comment = False) |
We should test with and without the In reply to: 1984112547 Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:32933 in cdd8f75. [](commit_id = cdd8f75, deletion_comment = False) |
Allocs are only reduced in the N = 1 or N = 2 case? Is that expected? Apologies to belabor things but perhaps we should also test a case where collection creation is followed up with some explicit Adds. |
Typo? Refers to: src/Compilers/CSharp/Test/Emit2/Semantics/CollectionExpressionTests.cs:33652 in 18b3599. [](commit_id = 18b3599, deletion_comment = False) |
Looking at the interactions between SetCount and List.Grow, they should be reduced for
This all started for us because we depend on a library that (over)uses List, and in many cases, we need List instances of only 1 element. |
It makes sense to me that some use cases depend heavily on numerous small Lists, and it feels reasonable to me for us to make an adjustment to this codegen in order to save the memory for those cases and increase uniformity of the observable behavior with the non-optimized case. I'll have to review the codegen changes in the tests in this PR to really feel 100% sure though. Thanks for taking a stab at this work :) |
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Show resolved
Hide resolved
@RikkiGibson I think the last 2 commits address both of your comments. There are tests in place that validate the different scenarios:
|
src/Compilers/CSharp/Portable/Lowering/LocalRewriter/LocalRewriter_CollectionExpression.cs
Outdated
Show resolved
Hide resolved
Thanks @jbevain! |
Fixes #72318
by lowering a collection expression with a known length to:
instead of
This ensures that the List is sized exactly for the knownLength.