Replies: 10 comments 6 replies
-
I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label. |
Beta Was this translation helpful? Give feedback.
-
It seems to be one of those features .NET team regrets adding in terms of both perf impact and compile-time safety References:
|
Beta Was this translation helpful? Give feedback.
-
Tagging subscribers to this area: @dotnet/area-system-runtime Issue DetailsProposal: Remove covariance on arrayPrev discussion: dotnet/csharplang#5153 Basically covariance on array allows us to upcast an array. But it also leads to the following issue: Base[] array = new Derived[10];
array[0] = new Base(); // exception here, type system is broken
class Base {}
class Derived : Base {} It's breaking the type system: a type error occurred in runtime regardless the compile-time type check. And also, an interesting case: Random[] a = new Random[1];
object[] c = a;
MemoryMarshal.GetArrayDataReference(c) = "a string";
var l = a[0].Next(); // guess what method is actually being called
Console.WriteLine(l); Covariance array is not only breaking the type system, but also has huge performance impact, let's see the benchmark (provided by @huoyaoyuan): class Base
{
}
class Derived : Base
{
}
[SimpleJob(RuntimeMoniker.Net60)]
public class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<Program>();
}
private readonly Base[] array = new Base[256];
private Derived derived = new();
private Base @base = new();
private readonly int[] intArray = new int[256];
[Benchmark]
public object Array()
{
for (int i = 0; i < array.Length; i++)
array[i] = derived;
return array;
}
[Benchmark]
public object ArrayBase()
{
for (int i = 0; i < array.Length; i++)
array[i] = @base;
return array;
}
[Benchmark]
public object ArrayInt()
{
for (int i = 0; i < intArray.Length; i++)
intArray[i] = 19260817;
return intArray;
}
[Benchmark]
public object SpanIndex()
{
Span<Base> span = array;
for (int i = 0; i < span.Length; i++)
span[i] = derived;
return array;
}
[Benchmark]
public object SpanRef()
{
Span<Base> span = array;
foreach (ref var x in span)
x = derived;
return array;
}
} Benchmark result:
The overhead of array covariance is significant. As you can see, array covariance makes code completely unsafe and can lead to undefined behavior, and it also makes writes to array much slower. Its benefit-harm ratio is minus: Benefits:
Harm:
To examine the usage of covariance array, I wrote an analyzer to help detect such usage in assembiles. I ran it against .NET + ASP.NET Core assembiles, and it turned out that the usage is rare. The analyzer: https://github.com/hez2010/ArrayUpcastAnalyzer Note that there may be some false positives, such as Most usages can be avoided via casting the type of element in array initializer (i.e. change Array is a fundamental thing, and its performance affects vast range of applications. Now that the usage of covariance on array is actually rare, let's remove covariance on array to deliver type-safe and high-performance array for everyone
|
Beta Was this translation helpful? Give feedback.
-
/cc @davidwrighton |
Beta Was this translation helpful? Give feedback.
-
This is not a defense of array covariance (I agree it is problematic). However, one place where removing it might cause issues is with the various reflection APIs for getting
Older code (especially things written pre-LINQ) might rely on this behavior; having it baked into the shape of these core APIs feels like a challenge. Hard to say how much of an issue this would be broadly. |
Beta Was this translation helpful? Give feedback.
-
That, or anything that's switching between collections with a common base, right? Animal[] animalsToWhatever = cats ? catArray : dogArray;
foreach (var a in animalsToWhatever)
{
a.Speak();
} (this seems a more likely and used scenario than dealing with reflection) |
Beta Was this translation helpful? Give feedback.
-
This isn't really actionable on the area owners end and should probably be a discussion thread instead. It would require sign-off from @jkotas and the various leads/PMs, etc. |
Beta Was this translation helpful? Give feedback.
-
MemoryMarshal is unsafe API. It is by design that you can do bad things with unsafe code.
This is good data, but I do not think it is complete as others pointed out. We tried to remove array covariance before in early days of .NET Native for UWP. It broke too much existing code and was added back. I would not mind introducing a unsupported runtime configuration switch to disable array covariance to enable experimentation in this area. |
Beta Was this translation helpful? Give feedback.
-
Actually, it would need to be a compiler switch too so that |
Beta Was this translation helpful? Give feedback.
-
I believe, array variance was added as a compatibility/migration feature for Java. It's probably a mistake... Another one that is at least as big is multicast delegates. I believe that was done to support events. Delegates should have been single-cast, and events should have been an |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Proposal: Remove covariance on array
Prev discussion: dotnet/csharplang#5153
Basically covariance on array allows us to upcast an array. But it also leads to the following issue:
It's breaking the type system: a type error occurred in runtime regardless the compile-time type check.
And also, an interesting case:
Covariance array is not only breaking the type system, but also has huge performance impact, let's see the benchmark (provided by @huoyaoyuan):
Benchmark result:
The overhead of array covariance is significant.
As you can see, array covariance makes code completely unsafe and can lead to undefined behavior, and it also makes writes to array much slower. Its benefit-harm ratio is minus:
Benefits:
Harm:
To examine the usage of covariance array, I wrote an analyzer to help detect such usage in assembiles. I ran it against .NET + ASP.NET Core assembiles, and it turned out that the usage is rare.
The analyzer: https://github.com/hez2010/ArrayUpcastAnalyzer
Here is the analysis report: report.txt
Note that there may be some false positives, such as
object[] array = (object[])obj;
whereobj
isobject
.Most usages can be avoided via casting the type of element in array initializer (i.e. change
object[] x = new T[] { new T() ... }
toobject[] x = new object[] { (object)new T()... }
), which can be done by Roslyn automatically, and after this change, the left usages are nearly none. So it's totally fine to remove covariance on array, since it has never been widely adopted.Array is a fundamental thing, and its performance affects vast range of applications. Now that the usage of covariance on array is actually rare, let's remove covariance on array to deliver type-safe and high-performance array for everyone
Approaches
I will prefer the first approach though it will introduce breaking changes, but we can have a guidance to help users migrate their code (basically find'n'replace). The second one seems doable but it requires users to adopt it manually, which makes no difference with today's
Span<>
: ton of code will still use the slow covariant arrays. While the third one is more aggressive, but it may make existing code hard to migrate.Beta Was this translation helpful? Give feedback.
All reactions