-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Open
Labels
area-System.LinquntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
Description
Description
Context
Initially this was noticed in a .NET 10 dotnet run .cs app that was using the ICSharpCode.Decompiler project, where we have sanity checks in our data structures to avoid certain modifications while the data is enumerated.
Analysis
- First, I noticed that the issue is not present in the ILSpy .NET 10 app, but can be consistently reproduced using
dotnet run .cs - Second, I discovered that the issue goes away if I target .NET 9 or lower in the
dotnet run .csapp. - At first, I didn't manage to reproduce the problem in a minimal app (see below), however, I noticed that
System.Linq.Enumerable.SizeOptIListSelectIterator2appears in the stack, when the problem occurs, butSystem.Linq.Enumerable.IListSelectIterator2appears in the stack, when it doesn't occur. - Then I looked at the source code of LINQ in .NET and noticed
System.Linq.Enumerable.IsSizeOptimized, which is a feature flag that causes theSizeOptIListSelectIteratorto be used, which causes the bug. - I can confirm that the problem goes away, if I set
UseSizeOptimizedLinqto false in the csproj or using#:propertyin thedotnet run .csapp.
Reproduction Steps
Reproducer:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UseSizeOptimizedLinq>true</UseSizeOptimizedLinq> <!-- if this is set to false the problem goes away -->
</PropertyGroup>
</Project>using System.Collections;
using System.Diagnostics;
Console.WriteLine("Hello, World!");
var collection = new SomeCollection<MyBase>();
var collection2 = new SomeCollection<MyBase>();
for (int i = 1; i <= 100; i++)
{
collection.Add(new MyBase { Id = i });
}
collection2.AddRange(collection.Select(x => x.Copy()));
collection.AssertNoActiveEnumerators(); // this fails
collection2.AssertNoActiveEnumerators();
foreach (var item in collection)
{
Console.WriteLine(item.Id);
}
collection.AssertNoActiveEnumerators();
foreach (var item in collection2)
{
Console.WriteLine(item.Id);
}
collection2.AssertNoActiveEnumerators();
class MyBase
{
public int Id { get; set; } = 0;
public MyBase Copy()
{
return new MyBase { Id = this.Id * this.Id };
}
}
sealed class SomeCollection<T> : IList<T>, IReadOnlyList<T> where T : MyBase
{
readonly List<T> _innerList = new();
public T this[int index] { get => ((IList<T>)_innerList)[index]; set => ((IList<T>)_innerList)[index] = value; }
public int Count => ((ICollection<T>)_innerList).Count;
public bool IsReadOnly => ((ICollection<T>)_innerList).IsReadOnly;
public void Add(T item)
{
((ICollection<T>)_innerList).Add(item);
}
public void Clear()
{
((ICollection<T>)_innerList).Clear();
}
public bool Contains(T item)
{
return ((ICollection<T>)_innerList).Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
((ICollection<T>)_innerList).CopyTo(array, arrayIndex);
}
StructEnumerator GetEnumerator()
{
return new StructEnumerator(this);
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public int IndexOf(T item)
{
return ((IList<T>)_innerList).IndexOf(item);
}
public void Insert(int index, T item)
{
((IList<T>)_innerList).Insert(index, item);
}
public bool Remove(T item)
{
return ((ICollection<T>)_innerList).Remove(item);
}
public void RemoveAt(int index)
{
((IList<T>)_innerList).RemoveAt(index);
}
struct StructEnumerator : IEnumerator<T>
{
private readonly SomeCollection<T> _collection;
private readonly IList<T> _list;
private int _index;
public StructEnumerator(SomeCollection<T> list)
{
this._collection = list;
this._list = list._innerList;
this._index = -1;
this._collection.NotifyStart();
}
public T Current => this._list[_index];
object IEnumerator.Current => Current;
public void Dispose()
{
this._collection.NotifyEnd();
}
public bool MoveNext()
{
return ++this._index < this._collection.Count;
}
public void Reset()
{
this._index = -1;
}
}
int currentEnumeratorCount = 0;
private void NotifyEnd()
{
Console.WriteLine("End: " + Environment.StackTrace);
Debug.Assert(currentEnumeratorCount > 0);
currentEnumeratorCount--;
}
private void NotifyStart()
{
Console.WriteLine("Start: " + Environment.StackTrace);
currentEnumeratorCount++;
}
public void AssertNoActiveEnumerators()
{
Debug.Assert(currentEnumeratorCount == 0);
}
}
static class Extensions
{
public static void AddRange<T>(this SomeCollection<T> collection, IEnumerable<T> items) where T : MyBase
{
foreach (var item in items)
{
collection.Add(item);
}
}
}Expected behavior
The Dispose method is called as in previous versions of .NET or without the UseSizeOptimizedLinq flag set.
Actual behavior
Crash:
Assertion failed.
currentEnumeratorCount == 0
at SomeCollection`1.AssertNoActiveEnumerators() in DisposableStructBug\Program.cs:line 157
at Program.<Main>$(String[] args) in DisposableStructBug\Program.cs:line 17
Regression?
Yes, as described above works in .NET 9 and earlier and in .NET 10 with the UseSizeOptimizedLinq flag set to false.
Known Workarounds
Set the UseSizeOptimizedLinq flag to false.
Configuration
- .NET 10.0.101 / Microsoft.NETCore.App 10.0.1
- Windows 11 Version 25H2 (OS Build 26200.7462)
- x64
Other information
Questions:
- Why is UseSizeOptimizedLinq enabled by default in
dotnet run .cs? This is a bit unexpected.
Related:
- I found that others have reported problems with
UseSizeOptimizedLinqtoo (Massive performance regression .NET 9 vs .NET 10Β #115033)
Copilot
Metadata
Metadata
Assignees
Labels
area-System.LinquntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner